Глава 18: Обзор стандартной библиотеки

Раздел 11: std::future — Основы работы с Future

Содержание: Введение в асинхронное программирование и Future Что такое Future? Как работает Future на низком уровне? Цикл опроса Почему Pin? Простой пример: Блокирующий Future Что происходит в коде? Реальный пример: Таймер с Waker Разбор кода Практические советы Упражнение: Реализация счётчика Решение Разбор упражнения Заключение

Добро пожаловать в мир асинхронного программирования в Rust! Сегодня мы разберём один из ключевых элементов асинхронности в этом языке — трейт std::future::Future. Эта лекция будет самодостаточной, подробной и ориентированной как на новичков, так и на тех, кто хочет углубить свои знания. Мы рассмотрим все нюансы, тонкости и практические аспекты работы с Future, снабдим вас примерами кода и практическим упражнением с избыточным покрытием и вниманием к деталям. Поехали!


Введение в асинхронное программирование и Future

Асинхронное программирование позволяет выполнять задачи без блокировки основного потока выполнения. Вместо того чтобы ждать завершения длительной операции (например, чтения файла или сетевого запроса), мы можем "отложить" её выполнение и заняться чем-то другим, а затем вернуться к результату, когда он будет готов. В Rust асинхронность построена вокруг концепции Future — абстракции, которая представляет собой операцию, результат которой будет доступен в будущем.

Трейт std::future::Future — это фундаментальный строительный блок асинхронного программирования в стандартной библиотеке Rust. Он был введён в язык для обеспечения низкоуровневой основы, которую можно использовать как в простых случаях, так и в сложных асинхронных runtime-системах, таких как Tokio или async-std.


Что такое Future?

Future — это объект, который описывает асинхронную операцию. Он может находиться в одном из трёх состояний:

  1. Pending (Ожидание): Операция ещё не завершена.
  2. Ready (Готово): Операция завершена, результат доступен.
  3. Полностью завершён: Future был опрошен до конца и больше не будет использоваться.

Формально Future — это трейт, определённый в стандартной библиотеке:

pub trait Future {
    type Output; // Тип результата, который возвращает Future
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Тип Poll — это перечисление:

pub enum Poll<T> {
    Ready(T),    // Результат готов
    Pending,     // Результат ещё не готов
}

Future не выполняется сам по себе — его нужно "двигать" вперёд, вызывая poll. Это делает либо исполнитель (executor), либо вы сами в простых случаях.


Как работает Future на низком уровне?

Давайте разберёмся, как Future взаимодействует с системой. Метод poll — это сердце трейта. Он принимает два аргумента:


Цикл опроса

  1. Вы или исполнитель вызываете poll.
  2. Если операция завершена, poll возвращает Poll::Ready(value).
  3. Если операция ещё не завершена, poll возвращает Poll::Pending, и исполнитель ждёт сигнала от Waker, чтобы снова вызвать poll.

Waker — это способ "разбудить" задачу. Например, если вы ждёте данные из сети, сетевой драйвер вызовет Waker, когда данные придут, и исполнитель снова опросит Future.


Почему Pin?

Pin — это гарантия, что Future не будет перемещён в памяти после начала работы. Некоторые Future могут содержать указатели на свои собственные поля (самоссылки), и перемещение сломает их. Pin решает эту проблему, "прикрепляя" объект к месту в памяти.


Простой пример: Блокирующий Future

Давайте начнём с простого примера, чтобы понять, как работать с Future вручную. Мы создадим Future, который имитирует задержку и возвращает число.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread;
use std::time::Duration;

struct Delay {
    delay_ms: u64,
    done: bool,
}

impl Future for Delay {
    type Output = i32; // Что вернём после завершения

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.done {
            return Poll::Ready(42); // Работа завершена
        }

        // Имитация асинхронной работы (блокирующий sleep для простоты)
        thread::sleep(Duration::from_millis(self.delay_ms));
        self.done = true;

        // Сообщаем, что результат готов
        Poll::Ready(42)
    }
}

fn main() {
    let delay = Delay { delay_ms: 1000, done: false };
    let future = delay;

    // Для простоты используем block_on из futures crate
    use futures::executor::block_on;
    let result = block_on(future);
    println!("Результат: {}", result); // Результат: 42
}

Что происходит в коде?

  1. Структура Delay: Содержит время задержки и флаг завершения.
  2. Реализация Future:
  3. block_on: Утилита из crates.io (futures), которая ждёт завершения Future. В стандартной библиотеке нет встроенного исполнителя, поэтому мы используем этот хак.

Важно: Этот пример блокирующий, что противоречит духу асинхронности. В реальном коде мы бы использовали неблокирующие операции (например, таймеры из Tokio), но для понимания основ он подходит.


Реальный пример: Таймер с Waker

Теперь давайте сделаем настоящий асинхронный Future, используя Waker для неблокирующего ожидания.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct AsyncTimer {
    delay_ms: u64,
    waker: Option<Arc<Mutex<Option<Waker>>>>,
    done: bool,
}

impl Future for AsyncTimer {
    type Output = String;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.done {
            return Poll::Ready("Время вышло!".to_string());
        }

        if self.waker.is_none() {
            // Создаём Waker и запускаем поток
            let waker = Arc::new(Mutex::new(Some(cx.waker().clone())));
            self.waker = Some(waker.clone());

            let delay_ms = self.delay_ms;
            thread::spawn(move || {
                thread::sleep(Duration::from_millis(delay_ms));
                let waker = waker.lock().unwrap().take();
                if let Some(waker) = waker {
                    waker.wake(); // Будим задачу
                }
            });
        }

        self.done = true;
        Poll::Pending // Первый вызов всегда Pending
    }
}

fn main() {
    use futures::executor::block_on;
    let timer = AsyncTimer {
        delay_ms: 1000,
        waker: None,
        done: false,
    };
    let result = block_on(timer);
    println!("Результат: {}", result); // Результат: Время вышло!
}

Разбор кода

  1. Поля структуры:
  2. poll:
  3. Arc и Mutex: Используются для безопасной передачи Waker между потоками.

Этот пример ближе к реальной асинхронности: поток не блокируется, а Future ждёт сигнала от Waker.


Практические советы

  1. Не используйте std::future в одиночку: В стандартной библиотеке Future — это низкоуровневый инструмент. Для реальной работы используйте runtime вроде Tokio или async-std, которые предоставляют таймеры, сетевое I/O и исполнители.
  2. Осторожно с блокировкой: Если poll блокирует поток (как в первом примере), вы теряете преимущества асинхронности.
  3. Pin и самоссылки: Если ваш Future сложный, изучите Pin и unsafe, чтобы правильно работать с самоссылающимися структурами.
  4. Тестирование: Используйте futures::executor::block_on для тестов, но в продакшене переходите на полноценный runtime.

Упражнение: Реализация счётчика

Создайте Future, который считает до заданного числа с интервалом в 1 секунду и возвращает строку "Готово!".


Решение

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct Counter {
    target: u32,
    current: u32,
    waker: Option<Arc<Mutex<Option<Waker>>>>,
}

impl Future for Counter {
    type Output = String;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.current >= self.target {
            return Poll::Ready("Готово!".to_string());
        }

        if self.waker.is_none() {
            let waker = Arc::new(Mutex::new(Some(cx.waker().clone())));
            self.waker = Some(waker.clone());

            let counter = Arc::new(Mutex::new(self.current));
            let target = self.target;
            let waker_clone = waker.clone();
            thread::spawn(move || {
                loop {
                    thread::sleep(Duration::from_secs(1));
                    let mut count = counter.lock().unwrap();
                    *count += 1;
                    println!("Счёт: {}", *count);
                    if *count >= target {
                        let waker = waker_clone.lock().unwrap().take();
                        if let Some(waker) = waker {
                            waker.wake();
                        }
                        break;
                    }
                }
            });
        }

        self.current += 1; // Обновляем счётчик после пробуждения
        if self.current >= self.target {
            Poll::Ready("Готово!".to_string())
        } else {
            Poll::Pending
        }
    }
}

fn main() {
    use futures::executor::block_on;
    let counter = Counter {
        target: 3,
        current: 0,
        waker: None,
    };
    let result = block_on(counter);
    println!("Результат: {}", result); // Результат: Готово!
}

Разбор упражнения


Заключение

Трейт std::future::Future — это основа асинхронного программирования в Rust. Он требует ручного управления через poll и Waker, что делает его низкоуровневым, но невероятно гибким. Мы изучили его структуру, создали простые и сложные примеры, а также решили практическое упражнение. В следующих разделах курса мы перейдём к async/await и runtime-системам, которые упрощают работу с Future. Пока же экспериментируйте с примерами и привыкайте к концепции опроса — это ключ к пониманию асинхронности в Rust!