Конечный автомат
Rust преобразует асинхронную функцию или блок в скрытый тип, который реализует Future, используя конечный автомат для отслеживания хода выполнения функции. Детали этого преобразования сложны, но полезно иметь схематическое представление о том, что происходит. Следующая функция
#![allow(unused)]
fn main() {
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
/// Сложить два броска D10 и модификатор.
async fn two_d10(modifier: u32) -> u32 {
let first_roll = roll_d10().await;
let second_roll = roll_d10().await;
first_roll + second_roll + modifier
}
}
преобразуется примерно в следующее:
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
/// Сложить два броска D10 и модификатор.
fn two_d10(modifier: u32) -> TwoD10 {
TwoD10::Init { modifier }
}
enum TwoD10 {
// Функция ещё не начала выполняться.
Init { modifier: u32 },
// Ожидаем завершения первого `.await`.
FirstRoll { modifier: u32, fut: RollD10Future },
// Ожидаем завершения второго `.await`.
SecondRoll { modifier: u32, first_roll: u32, fut: RollD10Future },
}
impl Future for TwoD10 {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
loop {
match *self {
TwoD10::Init { modifier } => {
// Создаём фьючерс для первого броска кубика.
let fut = roll_d10();
*self = TwoD10::FirstRoll { modifier, fut };
}
TwoD10::FirstRoll { modifier, ref mut fut } => {
// Опрашиваем подфьючерс для первого броска.
if let Poll::Ready(first_roll) = fut.poll(ctx) {
// Создаём фьючерс для второго броска.
let fut = roll_d10();
*self = TwoD10::SecondRoll { modifier, first_roll, fut };
} else {
return Poll::Pending;
}
}
TwoD10::SecondRoll { modifier, first_roll, ref mut fut } => {
// Опрашиваем подфьючерс для второго броска.
if let Poll::Ready(second_roll) = fut.poll(ctx) {
return Poll::Ready(first_roll + second_roll + modifier);
} else {
return Poll::Pending;
}
}
}
}
}
}
Этот пример иллюстративный и не является точным представлением преобразования компилятора Rust. Важно обратить внимание на следующее:
- Вызов асинхронной функции ничего не делает, кроме построения и возврата фьючерса.
- Все локальные переменные хранятся во фьючерсе функции; перечисление (enum) указывает, где выполнение сейчас приостановлено.
- Каждый
.awaitв асинхронной функции преобразуется в новое состояние, содержащее все живые переменные и ожидаемый фьючерс. Затемloopобрабатывает обновлённое состояние, опрашивая фьючерс, пока он не вернётPoll::Ready. - Выполнение продолжается без задержек, пока не случится
Poll::Pending. В этом простом примере каждый фьючерс готов сразу. - В
mainсодержится наивный исполнитель, который просто крутится в цикле, пока фьючерс не будет готов. Настоящие исполнители мы обсудим чуть позже.
Для дальнейшего изучения
Представьте структуру данных Future для глубоко вложенного стека асинхронных функций. Фьючерс каждой функции содержит структуры Future функций, которые она вызывает. Это может привести к неожиданно большим типам Future, генерируемым компилятором.
Это также означает, что рекурсивные асинхронные функции — задача непростая. Сравните с распространённой ошибкой построения рекурсивного типа, например
#![allow(unused)]
fn main() {
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
enum LinkedList<T> {
Node { value: T, next: LinkedList<T> },
Nil,
}
}
Исправление для рекурсивного типа — добавить слой косвенности, например с помощью Box. Аналогично рекурсивная асинхронная функция должна поместить рекурсивный фьючерс в Box:
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
async fn count_to(n: u32) {
if n > 0 {
Box::pin(count_to(n - 1)).await;
println!("{n}");
}
}