Фьючерсы (Futures)
Мы много говорили о фьючерсах в предыдущих главах; они являются ключевой частью асинхронного программирования в Rust! В этой главе мы углубимся в некоторые детали того, что такое фьючерсы, как они работают, и рассмотрим библиотеки для работы с фьючерсами напрямую.
Трейты Future и IntoFuture
- Future
- Ассоциированный тип
Output - Без реальных деталей здесь, опрос (polling) в следующем разделе, ссылка на продвинутые разделы о Pin, исполнителях (executors)/wakeraх
- Ассоциированный тип
- IntoFuture
- Использование - общее, в await, асинхронный строительный паттерн (builder pattern) (плюсы и минусы использования)
- Боксирование (Boxing) фьючерсов,
Box<dyn Future>и как это раньше было распространено и необходимо, но в основном не сейчас, за исключением рекурсии и т.д.
Опрос (Polling)
- что это такое и кто его выполняет, тип
PollReady- конечное состояние
- как это связано с
await drop= отмена (cancel)- для фьючерсов и, следовательно, задач
- последствия для асинхронного программирования в целом
- ссылка на главу о безопасности отмены (cancellation safety)
"Плавление" (Fusing)
Крейт futures-rs
- История и цель
- см. главу о потоках (streams)
- помощники для написания исполнителей (executors) или другого низкоуровневого кода с фьючерсами
- закрепление (pinning) и боксирование (boxing)
- исполнитель как частичная среда выполнения (см. альтернативные среды выполнения в справочнике)
- TryFuture
- удобные фьючерсы:
pending,ready,ok/errи т.д. - функции-комбинаторы в
FutureExt - альтернатива вещам из Tokio
- функции
- трейты IO
Крейт futures-concurrency
https://docs.rs/futures-concurrency/latest/futures_concurrency/
Фьючерсы — это основа асинхронного программирования в Rust. Понимание их внутреннего устройства необходимо для написания эффективного и корректного асинхронного кода.
Трейты Future и IntoFuture
Трейт Future
Трейт Future представляет собой асинхронную операцию, которая может производить значение в какой-то момент в будущем.
#![allow(unused)] fn main() { pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
Ассоциированный тип Output определяет тип значения, которое производит фьючерс при успешном завершении.
Метод poll используется для продвижения фьючерса вперед. Он возвращает Poll::Ready(Output), когда фьючерс завершен, или Poll::Pending, если он еще не готов.
Трейт IntoFuture
Трейт IntoFuture позволяет типам, которые не являются фьючерсами, быть преобразованными в фьючерсы. Это используется оператором .await для автоматического преобразования значений.
#![allow(unused)] fn main() { pub trait IntoFuture { type Output; type IntoFuture: Future<Output = Self::Output>; fn into_future(self) -> Self::IntoFuture; } }
Использование в await: Когда вы пишете expression.await, Rust автоматически вызывает into_future() для expression, если это необходимо.
Асинхронный строительный паттерн: IntoFuture позволяет создавать API, где методы возвращают не фьючерсы, а строительные объекты, которые затем преобразуются в фьючерсы:
#![allow(unused)] fn main() { struct QueryBuilder { ... } impl QueryBuilder { fn filter(self, condition: Filter) -> Self { ... } fn limit(self, n: usize) -> Self { ... } } impl IntoFuture for QueryBuilder { type Output = Result<Vec<Record>>; type IntoFuture = QueryFuture; fn into_future(self) -> Self::IntoFuture { QueryFuture::new(self) } } // Использование: let results = query_builder .filter(some_filter) .limit(10) .await?; }
Боксирование фьючерсов
Box<dyn Future<Output = T>> используется, когда вам нужна трейт-объект для фьючерса. Раньше это было необходимо для многих сценариев, но с появлением impl Trait и улучшенной работой компилятора с временами жизни, необходимость в этом значительно уменьшилась.
Текущие случаи использования:
- Рекурсивные фьючерсы
- Гетерогенные коллекции фьючерсов
- Динамическая диспетчеризация
#![allow(unused)] fn main() { // Рекурсивный фьючерс требует боксинга fn recursive_future(n: u32) -> BoxFuture<'static, u32> { async move { if n == 0 { 1 } else { recursive_future(n - 1).await + 1 } }.boxed() } }
Опрос (Polling)
Что такое опрос и кто его выполняет
Опрос — это механизм, с помощью которого фьючерс продвигается вперед. Исполнитель (executor) многократно вызывает метод poll фьючерса, пока он не вернет Poll::Ready.
Тип Poll:
#![allow(unused)] fn main() { pub enum Poll<T> { Ready(T), Pending, } }
Ready(T): Фьючерс завершен и произвел значениеTPending: Фьючерс еще не готов и должен быть опрошен позже
Связь с await
Оператор await скрывает сложность опроса от программиста. Когда вы пишете future.await, компилятор генерирует код, который:
- Опросит фьючерс
- Если
Pending, уступит управление планировщику - Когда фьючерс будет готов, продолжит выполнение
Удаление (Drop) = Отмена (Cancel)
Когда фьючерс удаляется (выходит из области видимости), он отменяется. Это имеет важные последствия:
Для фьючерсов: Любые ресурсы, удерживаемые фьючерсом, освобождаются, но фьючерс не получает уведомления об отмене.
Для задач: Когда задача отменяется, все ее фьючерсы удаляются, что может привести к потере данных, если фьючерс находился в середине операции.
Последствия для программирования: Необходимо проектировать фьючерсы так, чтобы они были безопасны к отмене (cancellation safe).
"Плавление" (Fusing)
Фьючерс, который после возврата Poll::Ready всегда должен возвращать Poll::Pending, называется "плавким" (fused). Это предотвращает неправильное повторное использование завершенного фьючерса.
#![allow(unused)] fn main() { use futures::future::Fuse; use futures::FutureExt; let mut future = async { 42 }.fuse(); // Первый опрос возвращает Ready(42) assert_eq!(future.poll(...), Poll::Ready(42)); // Все последующие опросы возвращают Pending assert_eq!(future.poll(...), Poll::Pending); }
Крейт futures-rs
История и цель
futures-rs — это foundational библиотека для асинхронного программирования в Rust. Она предоставляет:
- Базовые трейты и типы (
Future,Stream,Sink,AsyncRead,AsyncWrite) - Комбинаторы и утилиты для работы с фьючерсами
- Инфраструктуру для создания исполнителей и реакторов
Помощники для низкоуровневых операций
Помощники для закрепления (pinning):
#![allow(unused)] fn main() { use futures::pin_mut; let future = async { 42 }; pin_mut!(future); // Закрепляет future в памяти }
Боксирование:
#![allow(unused)] fn main() { use futures::future::BoxFuture; use futures::FutureExt; fn returns_boxed_future() -> BoxFuture<'static, i32> { async { 42 }.boxed() } }
Исполнитель как частичная среда выполнения
futures::executor предоставляет простой однопоточный исполнитель, полезный для тестирования и простых случаев:
#![allow(unused)] fn main() { use futures::executor::block_on; let result = block_on(async { some_async_function().await }); }
TryFuture
TryFuture — это фьючерс, который производит Result:
#![allow(unused)] fn main() { pub trait TryFuture: Future<Output = Result<Self::Ok, Self::Err>> { type Ok; type Err; } }
Комбинаторы TryFutureExt предоставляют методы для работы с фьючерсами, возвращающими Result.
Удобные фьючерсы
pending(): Фьючерс, который никогда не завершаетсяready(value): Немедленно завершающийся фьючерсok(value),err(error): Фьючерсы, возвращающиеResult
Функции-комбинаторы в FutureExt
FutureExt добавляет методы к любым типам, реализующим Future:
#![allow(unused)] fn main() { use futures::FutureExt; let future = async { 42 } .map(|x| x * 2) // Преобразование результата .then(|x| async { x }) // Цепочка с другим фьючерсом .boxed(); // Боксирование }
Альтернатива Tokio
futures-rs предоставляет альтернативные реализации для:
- Асинхронных трейтов ввода-вывода (
AsyncRead,AsyncWrite) - Каналов и примитивов синхронизации
- Таймеров и утилит времени
Крейт futures-concurrency
futures-concurrency предоставляет эргономичные API для конкурентного выполнения фьючерсов.
Основные возможности
Join для кортежей и массивов:
#![allow(unused)] fn main() { use futures_concurrency::prelude::*; let (a, b) = (future_a, future_b).join().await; let results = [future1, future2, future3].join().await; }
Race (гонка) фьючерсов:
#![allow(unused)] fn main() { use futures_concurrency::prelude::*; let result = (future_a, future_b).race().await; // Первый завершившийся }
Try-вариации:
#![allow(unused)] fn main() { use futures_concurrency::prelude::*; // try_join отменяет все фьючерсы при первой ошибке let result: Result<(A, B), E> = (future_a, future_b).try_join().await; }
Преимущества перед макросами
По сравнению с макросами join! и select!, futures-concurrency предлагает:
- Более чистый синтаксис
- Лучшую интеграцию с системами типов
- Поддержку динамических коллекций фьючерсов
#![allow(unused)] fn main() { // С динамической коллекцией let futures: Vec<impl Future<Output = i32>> = get_futures(); let results: Vec<i32> = futures.join().await; }
Этот крейт особенно полезен в случаях, когда набор фьючерсов определяется во время выполнения или когда нужна более сильная типизация.