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
— это объект, который описывает асинхронную операцию. Он может находиться в одном из трёх состояний:
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!