Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Конечный автомат

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;
                    }
                }
            }
        }
    }
}
This slide should take about 10 minutes.

Этот пример иллюстративный и не является точным представлением преобразования компилятора 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}");
    }
}