std::future — Основы работы с FutureFuture
Что такое Future?
Как работает Future на низком уровне?
Цикл опроса
Почему Pin?
Простой пример: Блокирующий Future
Что происходит в коде?
Реальный пример: Таймер с Waker
Разбор кода
Практические советы
Упражнение: Реализация счётчика
Решение
Разбор упражнения
Заключение
Добро пожаловать в мир асинхронного программирования в Rust! Сегодня мы разберём один из ключевых элементов асинхронности в этом языке — трейт std::future::Future. Эта лекция будет самодостаточной, подробной и ориентированной как на новичков, так и на тех, кто хочет углубить свои знания. Мы рассмотрим все нюансы, тонкости и практические аспекты работы с Future, снабдим вас примерами кода и практическим упражнением с избыточным покрытием и вниманием к деталям. Поехали!
FutureАсинхронное программирование позволяет выполнять задачи без блокировки основного потока выполнения. Вместо того чтобы ждать завершения длительной операции (например, чтения файла или сетевого запроса), мы можем "отложить" её выполнение и заняться чем-то другим, а затем вернуться к результату, когда он будет готов. В Rust асинхронность построена вокруг концепции Future — абстракции, которая представляет собой операцию, результат которой будет доступен в будущем.
Трейт std::future::Future — это фундаментальный строительный блок асинхронного программирования в стандартной библиотеке Rust. Он был введён в язык для обеспечения низкоуровневой основы, которую можно использовать как в простых случаях, так и в сложных асинхронных runtime-системах, таких как Tokio или async-std.
Future?Future — это объект, который описывает асинхронную операцию. Он может находиться в одном из трёх состояний:
Future был опрошен до конца и больше не будет использоваться.Формально Future — это трейт, определённый в стандартной библиотеке:
pub trait Future {
type Output; // Тип результата, который возвращает Future
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Output: Связанный тип, указывающий, что вернётся после завершения Future (например, i32, String или даже Result).poll: Метод, который "спрашивает" у Future, готов ли он. Возвращает Poll<T>, где T — это Output.Тип Poll — это перечисление:
pub enum Poll<T> {
Ready(T), // Результат готов
Pending, // Результат ещё не готов
}
Future не выполняется сам по себе — его нужно "двигать" вперёд, вызывая poll. Это делает либо исполнитель (executor), либо вы сами в простых случаях.
Future на низком уровне?Давайте разберёмся, как Future взаимодействует с системой. Метод poll — это сердце трейта. Он принимает два аргумента:
self: Pin<&mut Self>: Ссылка на сам Future, закреплённая в памяти (Pin нужен для безопасности при работе с самоссылающимися структурами, но об этом чуть позже).cx: &mut Context<'_>: Контекст, который предоставляет доступ к Waker — механизму, позволяющему сообщить исполнителю, что Future готов продолжить работу.poll.poll возвращает Poll::Ready(value).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
}
Delay: Содержит время задержки и флаг завершения.Future:
type Output = i32: Возвращаем число 42.poll: Если работа не завершена (done == false), ждём (в данном случае блокирующе), затем меняем флаг и возвращаем Poll::Ready(42).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); // Результат: Время вышло!
}
delay_ms: Время задержки.waker: Хранит Waker для пробуждения задачи.done: Флаг завершения.poll:
done == true, возвращаем результат.waker ещё не создан, запускаем поток, который ждёт указанное время и затем вызывает wake() на Waker.Poll::Pending, второй (после пробуждения) — Poll::Ready.Arc и Mutex: Используются для безопасной передачи Waker между потоками.Этот пример ближе к реальной асинхронности: поток не блокируется, а Future ждёт сигнала от Waker.
std::future в одиночку: В стандартной библиотеке Future — это низкоуровневый инструмент. Для реальной работы используйте runtime вроде Tokio или async-std, которые предоставляют таймеры, сетевое I/O и исполнители.poll блокирует поток (как в первом примере), вы теряете преимущества асинхронности.Pin и самоссылки: Если ваш Future сложный, изучите Pin и unsafe, чтобы правильно работать с самоссылающимися структурами.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); // Результат: Готово!
}
Counter: Считает от 0 до target, увеличивая current каждую секунду.wake() по завершении.Трейт std::future::Future — это основа асинхронного программирования в Rust. Он требует ручного управления через poll и Waker, что делает его низкоуровневым, но невероятно гибким. Мы изучили его структуру, создали простые и сложные примеры, а также решили практическое упражнение. В следующих разделах курса мы перейдём к async/await и runtime-системам, которые упрощают работу с Future. Пока же экспериментируйте с примерами и привыкайте к концепции опроса — это ключ к пониманию асинхронности в Rust!