Определения
Асинхронность
В контексте Rust асинхронный код относится к коду, который использует функцию языка async/await, позволяющую многим задачам работать конкурентно на нескольких потоках (или даже одном потоке).
Конкурентность и параллелизм
Конкурентность и параллелизм — это два связанных понятия, которые используются при обсуждении выполнения нескольких задач одновременно. Если что-то происходит параллельно, то это также происходит конкурентно, но обратное неверно: переключение между двумя задачами без фактической одновременной работы над ними — это конкурентность, но не параллелизм.
Future (Футура/Будущее)
Future — это значение, которое хранит текущее состояние некоторой операции. Future также имеет метод poll, который позволяет операции продолжаться до тех пор, пока ей не потребуется ожидать чего-либо, например, сетевого соединения. Вызовы метода poll должны возвращаться очень быстро.
Future часто создаются путем комбинирования нескольких future с использованием .await в async-блоке.
Исполнитель/Планировщик
Исполнитель или планировщик — это нечто, что выполняет future, многократно вызывая метод poll. В стандартной библиотеке нет исполнителя, поэтому для этого нужна внешняя библиотека, и наиболее широко используемый исполнитель предоставляется средой выполнения Tokio.
Исполнитель способен запускать большое количество future конкурентно на нескольких потоках. Он делает это, переключая текущую задачу в точках await. Если код проводит много времени без достижения .await, это называется "блокировкой потока" или "не возвращением управления исполнителю", что мешает запуску других задач.
Среда выполнения
Среда выполнения — это библиотека, которая содержит исполнитель вместе с различными утилитами, интегрированными с этим исполнителем, такими как утилиты времени и ввода-вывода. Слова "среда выполнения" и "исполнитель" иногда используются взаимозаменяемо. В стандартной библиотеке нет среды выполнения, поэтому для этого нужна внешняя библиотека, и наиболее широко используемой средой выполнения является среда выполнения Tokio.
Слово "Runtime" также используется в других контекстах, например, фраза "У Rust нет runtime" иногда означает, что Rust не выполняет сборку мусора или JIT-компиляцию.
Задача
Задача — это операция, выполняемая в среде выполнения Tokio, создаваемая функцией tokio::spawn или Runtime::block_on. Инструменты для создания future путем их комбинирования, такие как .await и join!, не создают новых задач, и каждая комбинируемая часть считается "в той же задаче".
Для параллелизма требуется несколько задач, но возможно конкурентно выполнять несколько операций в одной задаче с помощью таких инструментов, как join!.
Порождающее создание
Порождающее создание — это когда функция tokio::spawn используется для создания новой задачи. Это также может относиться к созданию нового потока с помощью std::thread::spawn.
Async-блок
Async-блок — это простой способ создать future, который выполняет некоторый код. Например:
#![allow(unused)] fn main() { let world = async { println!(" world!"); }; let my_future = async { print!("Hello "); world.await; }; }
Код выше создает future с именем my_future, который при выполнении печатает Hello world!. Он делает это, сначала печатая "Hello", а затем выполняя future world. Обратите внимание, что сам по себе этот код ничего не печатает — вам нужно фактически выполнить my_future, либо напрямую создав его как задачу, либо используя .await в чем-то, что вы создаете.
Async-функция
Подобно async-блоку, async-функция — это простой способ создать функцию, тело которой становится future. Все async-функции можно переписать в обычные функции, возвращающие future:
#![allow(unused)] fn main() { async fn do_stuff(i: i32) -> String { // do stuff format!("The integer is {}.", i) } }
#![allow(unused)] fn main() { use std::future::Future; // async-функция выше аналогична этой: fn do_stuff(i: i32) -> impl Future<Output = String> { async move { // do stuff format!("The integer is {}.", i) } } }
Здесь используется синтаксис impl Trait для возврата future, поскольку Future — это трейт. Обратите внимание, что поскольку future, созданный async-блоком, ничего не делает до своего выполнения, вызов async-функции ничего не делает до тех пор, пока не будет выполнен возвращаемый future (игнорирование этого вызывает предупреждение).
Уступка управления
В контексте асинхронного Rust уступка управления — это то, что позволяет исполнителю выполнять множество future в одном потоке. Каждый раз, когда future уступает управление, исполнитель может заменить этот future другим future, и, многократно переключая текущую задачу, исполнитель может конкурентно выполнять большое количество задач. Future может уступить управление только в .await, поэтому future, которые проводят много времени между .await, могут препятствовать выполнению других задач.
Если конкретно, future уступает управление, когда возвращается из метода poll.
Блокировка
Слово "блокировка" используется двумя разными способами: первое значение "блокировки" — это просто ожидание завершения чего-либо, а другое значение блокировки — когда future проводит много времени без уступки управления. Для однозначности можно использовать фразу "блокировка потока" для второго значения.
Документация Tokio всегда будет использовать второе значение "блокировки".
Для выполнения блокирующего кода в Tokio см. раздел Задачи, ограниченные CPU, и блокирующий код в справочнике по API Tokio.
Поток
Stream — это асинхронная версия Iterator, предоставляющая поток значений. Он обычно используется вместе с циклом while let, например:
#![allow(unused)] fn main() { use tokio_stream::StreamExt; // для next() async fn dox() { let mut stream = tokio_stream::empty::<()>(); while let Some(item) = stream.next().await { // что-то делаем } } }
Слово "поток" иногда по ошибке используется для обозначения трейтов AsyncRead и AsyncWrite.
Утилиты для потоков Tokio в настоящее время предоставляются крейтом tokio-stream. Когда трейт Stream стабилизируется в std, утилиты потоков будут перемещены в крейт tokio.
Канал
Канал — это инструмент, который позволяет одной части кода отправлять сообщения другим частям. Tokio предоставляет несколько каналов, каждый из которых служит своей цели.
- mpsc: многопоточный отправитель, однопоточный получатель. Можно отправить много значений.
- oneshot: однопоточный отправитель, однопоточный получатель. Можно отправить одно значение.
- broadcast: многопоточный отправитель, многопоточный получатель. Можно отправить много значений. Каждый получатель видит каждое значение.
- watch: однопоточный отправитель, многопоточный получатель. Можно отправить много значений, но история не сохраняется. Получатели видят только последнее значение.
Если вам нужен многопоточный отправитель/многопоточный получатель канала, где только один получатель видит каждое сообщение, вы можете использовать крейт async-channel.
Также существуют каналы для использования вне асинхронного Rust, такие как std::sync::mpsc и crossbeam::channel. Эти каналы ожидают сообщения, блокируя поток, что не допускается в асинхронном коде.
Обратное давление
Обратное давление — это шаблон проектирования приложений, которые хорошо справляются с высокой нагрузкой. Например, канал mpsc бывает как ограниченным, так и неограниченным. Используя ограниченный канал, получатель может создавать "обратное давление" на отправителя, если получатель не успевает обрабатывать количество сообщений, что позволяет избежать неограниченного роста использования памяти при отправке все большего количества сообщений по каналу.
Актор
Шаблон проектирования для создания приложений. Актор — это независимо созданная задача, которая управляет некоторым ресурсом от имени других частей приложения, используя каналы для связи с этими другими частями приложения.
См. главу о каналах для примера актора.