状態機械

Rust は async 関数またはブロックを、関数の進行状況を追跡するために状態機械を使って Future を実装する隠れた型へと変換します。この変換の詳細は複雑ですが、何が起きているのかを概略的に理解しておくことには意味があります。次の関数は

#![allow(unused)]
fn main() {
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

/// 2 回の 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};

/// 2 回の D10 ロールの合計に修正値を加えます。
fn two_d10(modifier: u32) -> TwoD10 {
    TwoD10::Init { modifier }
}

enum TwoD10 {
    // 関数はまだ開始されていません。
    Init { modifier: u32 },
    // 最初の `.await` が完了するのを待っています。
    FirstRoll { modifier: u32, fut: RollD10Future },
    // 2 回目の `.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 } => {
                    // 最初のダイスロール用の Future を作成します。
                    let fut = roll_d10();
                    *self = TwoD10::FirstRoll { modifier, fut };
                }
                TwoD10::FirstRoll { modifier, ref mut fut } => {
                    // 最初のダイスロール用のサブ Future を poll します。
                    if let Poll::Ready(first_roll) = fut.poll(ctx) {
                        // 2 回目のロール用の Future を作成します。
                        let fut = roll_d10();
                        *self = TwoD10::SecondRoll { modifier, first_roll, fut };
                    } else {
                        return Poll::Pending;
                    }
                }
                TwoD10::SecondRoll { modifier, first_roll, ref mut fut } => {
                    // 2 回目のダイスロール用のサブ Future を poll します。
                    if let Poll::Ready(second_roll) = fut.poll(ctx) {
                        return Poll::Ready(first_roll + second_roll + modifier);
                    } else {
                        return Poll::Pending;
                    }
                }
            }
        }
    }
}

この例は説明のためのものであり、Rust コンパイラの変換を正確に表したものではありません。ここで注目すべき重要な点は次のとおりです。

  • async 関数を呼び出しても、Future を構築して返すだけで、それ以外は何も起こりません。
  • すべてのローカル変数は、その関数の Future の中に格納され、enum を使って実行が現在どこで中断されているかを識別します。
  • async 関数内の .await は、生存中のすべての変数と待機対象の Future を含む新しい状態へと変換されます。その後 loop がその更新された状態を処理し、Future が Poll::Ready を返すまで poll し続けます。
  • 実行は Poll::Pending が発生するまでそのまま進み続けます。この単純な例では、すべての Future が即座に ready になります。
  • main には素朴なエグゼキュータが含まれており、Future の準備ができるまで単にビジーループします。実際のエグゼキュータについては後ほど説明します。

さらに掘り下げる

深くネストした async 関数のスタックに対する Future データ構造を想像してみてください。各関数の Future には、その関数が呼び出す関数の Future 構造が含まれます。その結果、コンパイラが生成する Future 型が予想外に大きくなることがあります。

これはまた、再帰的な async 関数が難しいことも意味します。たとえば、次のように再帰型を構築してしまう一般的なエラーと比べてみてください。

#![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 のように間接参照の層を 1 つ追加することです。同様に、再帰的な async 関数では再帰する Future をボックス化しなければなりません。

// 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}");
    }
}