Язык программирования Rust
Скачать 7.02 Mb.
|
Сигнализация потокам прекратить прослушивание получения задач Теперь со всеми внесёнными нами изменениями код компилируется без каких-либо предупреждений. Но плохая новость в том, что этот код ещё не работает так, как мы этого хотим. Ключом является логика в замыканиях, запускаемых потоками экземпляров Worker . В данный момент мы вызываем join , но это не завершит потоки, потому что они работают в цикле loop бесконечно в поиске новой задачи (job). Если мы попытаемся удалить ThreadPool в текущей реализации drop , то основной поток навсегда заблокируется в ожидании завершения первого потока из пула. Для решения этой проблемы, мы изменим потоки так, чтобы они прослушивали либо задачи Job для её выполнения, либо сигнал, что они должны прекратить прослушивание и выйти из бесконечного цикла. Вместо отправки экземпляров задач Job , наш канал отправит один из этих двух вариантов перечисления. Файл: src/lib.rs Это перечисление Message будет либо вариантом NewJob , который внутри держит Job с потоком для выполнения, или это будет вариант Terminate , который сделает так, чтобы поток вышел из цикла и остановился. Нам нужно настроить канал для использования значений типа Message , а не типа Job как показано в листинге 20-23. Файл: src/lib.rs impl Drop for ThreadPool { fn drop (& mut self ) { for worker in & mut self .workers { println! ( "Shutting down worker {}" , worker.id); if let Some (thread) = worker.thread.take() { thread.join().unwrap(); } } } } {{#rustdoc_include ../listings/ch20-web-server/no-listing- 07 -define-message- enum / src /lib.rs:here}} Листинг 20-23: Отправка и получение значений перечисления Message и выход из цикла, если Worker получает перечисление Message::Terminate Чтобы встроить в код перечисление Message , нужно изменить Job на Message в двух местах: в объявлении ThreadPool и сигнатуре Worker::new . Метод execute у ThreadPool должен отправлять задания, завёрнутые в вариант Message::NewJob . Затем в Worker::new , где Message получен из канала, задание (job) будет обработано, если получен вариант NewJob и поток выйдет из цикла, если получен вариант Terminate С такими изменениями код компилируется и продолжит функционировать так же, как и после кода 20-20. Но мы получим предупреждение, потому что мы не создаём сообщения типа Terminate . Давайте исправим это предупреждение, изменив нашу реализацию Drop как в листинге 20-25. pub struct ThreadPool { workers: Vec Option } // --snip-- impl ThreadPool { pub fn new (size: usize ) -> ThreadPool { // --snip-- ThreadPool { workers, sender: Some (sender), } } pub fn execute self , f: F) where F: FnOnce () + Send + 'static , { let job = Box ::new(f); self .sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop (& mut self ) { drop ( self .sender.take()); for worker in & mut self .workers { println! ( "Shutting down worker {}" , worker.id); if let Some (thread) = worker.thread.take() { thread.join().unwrap(); } } } } Файл: src/lib.rs Листинг 20-24: Отправка Message::Terminate рабочим потокам перед вызовом join в каждом потоке Теперь мы дважды проходим по потокам "работников": один раз для отправки одного сообщения Terminate каждому потоку и один раз для вызова join у каждого рабочего потока. Если бы мы попытались отправить сообщение и сразу же выполнить join в этом же цикле, мы не смогли бы гарантировать, что рабочий поток в текущей итерации получит сообщение из канала. Чтобы лучше понять, почему нужны два отдельных цикла, представьте сценарий с двумя работниками. Если бы мы использовали один цикл для перебора каждого работника, на первой итерации сообщение о прекращении работы было бы отправлено в канал и метод join вызывался бы у первого рабочего потока. Если этот первый поток занят обработкой запроса в данный момент, второй рабочий поток получит сообщение о завершении из канала и завершит свою работу. В главном потоке мы бы остались в ожидании завершения работы первого рабочего потока, но этого не произошло, потому что второй поток получил сообщение о завершении. Это ситуация взаимной блокировки (deadlock)! Чтобы предотвратить такой сценарий, мы сначала помещаем все сообщения Terminate в канал в первом цикле; затем мы объединяем (join) для завершения все потоки во втором цикле. Каждый рабочий поток прекратит получать запросы из канала, как только получит сообщение о завершении. Таким образом, мы можем быть уверены, что если мы отправим количество завершающих сообщений равное количеству рабочих потоков то, каждый получит сообщение о завершении до вызова join в его потоке. impl Worker { fn new (id: usize , receiver: Arc move || loop { match receiver.lock().unwrap().recv() { Ok (job) => { println! ( "Worker {id} got a job; executing." ); job(); } Err (_) => { println! ( "Worker {id} disconnected; shutting down." ); break ; } } }); Worker { id, thread: Some (thread), } } } Чтобы увидеть этот код в действии, давайте изменим main , чтобы принимать только два запроса, прежде чем корректно завершить работу сервера как показано в листинге 20- 25. Файл: src/bin/main.rs Код 20-25. Выключение сервера после обслуживания двух запросов с помощью выхода из цикла Вы бы не хотели, чтобы реальный веб-сервер отключался после обслуживания только двух запросов. Этот код всего лишь демонстрирует, что корректное завершение работы и освобождение ресурсов находятся в рабочем состоянии. Метод take определён в типаже Iterator и ограничивает итерацию максимум первыми двумя элементами. ThreadPool выйдет из области видимости в конце main и будет запущена его реализация drop Запустите сервер с cargo run и сделайте три запроса. Третий запрос должен выдать ошибку и в терминале вы должны увидеть вывод, подобный следующему: Вы возможно увидите другой порядок рабочих потоков и напечатанных сообщений. Мы можем увидеть, как этот код работает по сообщениям: "работники" номер 0 и 3 получили первые два запроса, затем на третьем запросе сервер прекратил принимать соединения. Когда ThreadPool выходит из области видимости в конце main , то срабатывает его реализация типажа Drop и пул сообщает всем рабочим потокам прекратить выполнение. Каждый рабочий поток распечатывает сообщение, когда видит сообщение о завершении, а затем пул потоков вызывает join , чтобы завершить работу каждого рабочего потока. {{#rustdoc_include ../listings/ch20-web-server/listing- 20 - 25 /src/bin/main.rs:here}} $ cargo run Compiling hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 1.0s Running `target/debug/main` Worker 0 got a job; executing. Worker 3 got a job; executing. Shutting down. Sending terminate message to all workers. Shutting down all workers. Shutting down worker 0 Worker 1 was told to terminate. Worker 2 was told to terminate. Worker 0 was told to terminate. Worker 3 was told to terminate. Shutting down worker 1 Shutting down worker 2 Shutting down worker 3 Обратите внимание на один интересный аспект этого конкретного выполнения: ThreadPool отправил сообщения о завершении в канал и прежде чем какой-либо рабочий получил сообщение, мы пытались присоединить (join) "работника" с номером 0. Рабочий поток 0 ещё не получил сообщение о прекращении, поэтому основной поток заблокировал ожидание потока 0 для завершения. Тем временем каждый из рабочих потоков получил сообщения об завершении. Когда рабочий поток 0 завершился, основной поток ждал окончания завершения выполнения остальных рабочих потоков. В этот момент все они получили сообщение о завершении и смогли завершиться. Поздравления! Теперь мы завершили проект; у нас есть базовый веб-сервер, который использует пул потоков для асинхронных ответов. Мы можем выполнить корректное завершение работы сервера, который очищает все потоки в пуле. Вот полный код для справки: Файл: src/bin/main.rs Файл: src/lib.rs В коде можно сделать больше! Если вы хотите продолжить совершенствование этого проекта, вот несколько идей: Добавьте больше документации в ThreadPool и его публичным методам. Добавьте тесты функционалу из библиотеки. Заменить вызовы unwrap на более правильную обработку ошибок. Используйте ThreadPool для выполнения некоторых других задач, помимо обслуживания веб-запросов. Найдите крейт для пула потоков на crates.io и реализуйте аналогичный веб-сервер, используя такой крейт. Затем сравните его API и надёжность с пулом потоков, который мы реализовали. Итоги Отличная работа! Вы сделали это к концу книги! Мы хотим поблагодарить вас за то, что присоединились к нам в этом путешествии по языку Rust. Теперь вы готовы реализовать свои собственные проекты на Rust и помочь с проектами другим людям. Имейте в виду, что существует приветливое сообщество других Rust разработчиков, которые хотели бы помочь вам с любыми сложными задачами с которыми вы столкнётесь в своём Rust путешествии. {{#rustdoc_include ../listings/ch20-web-server/no-listing- 08 - final - code/src/bin/main.rs}} {{#rustdoc_include ../listings/ch20-web-server/no-listing- 08 - final - code/src/lib.rs}} Дополнительная информация Следующие разделы содержат справочные материалы, которые могут оказаться полезными в вашем путешествии по Rust. Приложение A: Ключевые слова Следующий список содержит ключевые слова, зарезервированные для текущего или будущего использования в языке Rust. Как таковые их нельзя использовать в качестве идентификаторов (за исключением сырых идентификаторов, которые мы обсудим в разделе «Сырые идентификаторы» ). Идентификаторы — это имена функций, переменных, параметров, полей структур, модулей, крейтов, констант, макросов, статических значений, атрибутов, типов, свойств или времён жизни. Ключевые слова, использующиеся в Rust в настоящее время Ниже приведён список используемых в настоящее время ключевых слов с описанием их функций. as — выполнить примитивное преобразование, уточнить конкретную характеристику, которую содержит объект, или переименовать элемент в выражении use async — вернуть Future вместо блокирования текущего потока await — приостановить выполнение до тех пор, пока не будет готов результат Future break — немедленно завершить цикл const — определить константу или неизменяемый указатель continue — досрочно перейти к следующей итерации цикла crate — в пути модуля ссылается на корень пакета dyn — динамическая отсылка к объекту характеристики else — ветвь для конструкций потока управления if и if let в случае, если никакая другая ветвь не была исполнена enum — определить перечисление extern — подключить внешнюю функцию или переменную false — логический литерал «ложь» fn — определить функцию или тип указателя на функцию for — перебор элементов итератора, реализация характеристики или указание срока жизни более продолжительного периода if — ветвление на основе результата условного выражения impl — реализовать функциональность непосредственно или через характеристику in — часть синтаксиса определения цикла for let — объявить переменную loop — безусловный (бесконечный) цикл match — сопоставить значение с образцами mod — определить модуль move — заставить замыкание принять на себя владение всеми своими захватами mut — обозначить изменяемость для ссылок, сырых указателей или привязок к шаблонам pub — обозначить публичную доступность полей структур, блоков impl или модулей ref — привязка по ссылке return — возврат из функции Self — псевдоним для типа, который мы определяем или реализуем self — объект, содержащий этот метод или текущий модуль static — глобальная переменная или время жизни на протяжении всего выполнения программы struct — определить структуру super — родительский модуль текущего модуля trait — определить характеристику true — логический литерал «истина» type — определить псевдоним типа или ассоциированный тип union — определение union ; является ключевым словом только при использовании в объявлении союза unsafe — обозначить небезопасный код, функции, характеристики или реализации use — ввести объекты в область действия where — обозначить утверждения, которые ограничивают тип while — цикл, работающий относительно результата условного выражения Ключевые слова, зарезервированные для будущего использования Следующие ключевые слова ещё не имеют никакой функциональности, но зарезервированы Rust для возможного использования в будущем. abstract become box do final macro override priv try typeof unsized virtual yield Сырые идентификаторы Сырые идентификаторы — это синтаксис, позволяющий использовать ключевые слова там, где обычно они не могут быть. Для создания и использования сырого идентификатора к ключевому слову добавляется префикс r# Например, ключевое слово match . Если вы попытаетесь скомпилировать следующую функцию, использующую в качестве имени match : Файл: src/main.rs вы получите ошибку: Ошибка говорит о том, что вы не можете использовать ключевое слово match в качестве идентификатора функции. Чтобы получить возможность использования слова match в качестве имени функции, нужно использовать синтаксис «сырых идентификаторов», например так: Файл: src/main.rs Этот код скомпилируется без ошибок. Обратите внимание, что префикс r# в определении имени функции указан так же, как он указан в месте её вызова в main Сырые идентификаторы позволяют вам использовать любое слово, которое вы выберете, в качестве идентификатора, даже если это слово окажется зарезервированным ключевым словом. Это даёт нам больше свободы в выборе имён идентификаторов, а также позволяет нам интегрироваться с программами, написанными на языке, где эти слова не являются ключевыми. Кроме того, необработанные идентификаторы позволяют вам использовать библиотеки, написанные в версии Rust, отличной от используемой в вашем крейте. Например, try не является ключевым словом в выпуске 2015 года, но есть в выпуске 2018 года. Если вы зависите от библиотеки, написанной с использованием версии 2015 года и имеющей функцию try , вам потребуется использовать синтаксис сырого идентификатора, в данном случае r#try , для вызова этой функции из кода fn match (needle: & str , haystack: & str ) -> bool { haystack.contains(needle) } error: expected identifier, found keyword `match` --> src/main.rs:4:4 | 4 | fn match(needle: &str, haystack: &str) -> bool { | ^^^^^ expected identifier, found keyword fn r # match (needle: & str , haystack: & str ) -> bool { haystack.contains(needle) } fn main () { assert! (r# match ( "foo" , "foobar" )); } версии 2018 года. См. Приложение E для получения дополнительной информации о редакциях Rust. Операторы_и_обозначения'>Дополнение Б: Операторы и обозначения Это дополнение содержит глоссарий синтаксиса Rust, включая операторы и другие обозначения, которые появляются сами по себе или в контексте путей, обобщений, типажей, макросов, атрибутов, комментариев, кортежей и скобок. Операторы Таблица Б-1 содержит операторы языка Rust, пример появления оператора, короткое объяснение, возможность перегрузки оператора. Если оператор можно перегрузить, то показан типаж, с помощью которого его можно перегрузить. Таблица Б-1: Операторы Оператор Пример Объяснение Перегружаемость ! ident!(...) , ident!{...} , ident![...] Вызов макроса ! !expr Побитовое или логическое отрицание Not != expr != expr Сравнение "не равно" PartialEq % expr % expr Остаток от деления Rem %= var %= expr Остаток от деления и присваивание RemAssign & &expr , &mut expr Заимствование & &type , &mut type , &'a type , &'a mut type Указывает что данный тип заимствуется & expr & expr Побитовое И BitAnd &= var &= expr Побитовое И и присваивание BitAndAssign && expr && expr Логическое И * expr * expr Арифметическое умножение Mul *= var *= expr Арифметическое умножение и присваивание MulAssign Оператор Пример Объяснение Перегружаемость * *expr Разыменование ссылки Deref * *const type , *mut type Указывает, что данный тип является сырым указателем + trait + trait , 'a + trait Соединение ограничений типа + expr + expr Арифметическое сложение Add += var += expr Арифметическое сложение и присваивание AddAssign , expr, expr Аргумент и разделитель элементов - - expr Арифметическое отрицание Neg - expr - expr Арифметическое вычитание Sub - var -= expr Арифметическое вычитание и присваивание SubAssign -> fn(...) -> type , |...| -> type expr.ident Доступ к элементу , expr.. , ..expr , expr..expr Указывает на диапазон чисел, исключая правый PartialOrd ..= ..=expr , expr..=expr Указывает на диапазон чисел, включая правый PartialOrd ..expr Синтаксис обновления структуры variant(x, ..) , struct_type { x, .. } Привязка «И все остальное» |