Фьючерсы (Futures)

Мы много говорили о фьючерсах в предыдущих главах; они являются ключевой частью асинхронного программирования в Rust! В этой главе мы углубимся в некоторые детали того, что такое фьючерсы, как они работают, и рассмотрим библиотеки для работы с фьючерсами напрямую.

Трейты Future и IntoFuture

  • Future
    • Ассоциированный тип Output
    • Без реальных деталей здесь, опрос (polling) в следующем разделе, ссылка на продвинутые разделы о Pin, исполнителях (executors)/wakeraх
  • IntoFuture
    • Использование - общее, в await, асинхронный строительный паттерн (builder pattern) (плюсы и минусы использования)
  • Боксирование (Boxing) фьючерсов, Box<dyn Future> и как это раньше было распространено и необходимо, но в основном не сейчас, за исключением рекурсии и т.д.

Опрос (Polling)

  • что это такое и кто его выполняет, тип Poll
    • Ready - конечное состояние
  • как это связано с 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): Фьючерс завершен и произвел значение T
  • Pending: Фьючерс еще не готов и должен быть опрошен позже

Связь с await

Оператор await скрывает сложность опроса от программиста. Когда вы пишете future.await, компилятор генерирует код, который:

  1. Опросит фьючерс
  2. Если Pending, уступит управление планировщику
  3. Когда фьючерс будет готов, продолжит выполнение

Удаление (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;
}

Этот крейт особенно полезен в случаях, когда набор фьючерсов определяется во время выполнения или когда нужна более сильная типизация.