このページの目次

async/await の内部メカニズム

async fn は、Future トレイトを実装するステートマシンにコンパイラによって展開されます。各 .await ポイントは1つの状態です。PinFuture がメモリ上で移動しないこと(自己参照構造)を保証し、Waker は実行系(executor)に次のポーリングをいつ行えるかを伝えます。この展開プロセスを理解すれば、Rust 非同期処理の「ゼロコスト抽象」の仕組みと、Future がなぜ安易に drop してはいけないかが理解できます。

async fn はコンパイル後にどうなるか

async fn read_config() -> Config {
    let file = read_file().await;      // await point 1
    parse(&file).await                 // await point 2
}

コンパイラは async fnFuture トレイトを実装する匿名型(ステートマシン)に変換します:

// コンパイラによって生成される擬似コード:
enum ReadConfigFuture {
    State0 { ... },                    // 初期状態、read_file を呼び出す直前
    State1 { file_content: String },   // read_file の完了を待機中、file_content を保持
    State2 { file: String },           // parse の完了を待機中
    Done,
}

impl Future for ReadConfigFuture {
    type Output = Config;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Config> {
        match self {
            State0 => { /* read_file を開始、State1 に遷移 */ Poll::Pending },
            State1 { file_content } => { /* read_file が完了したか確認 */ ... },
            // ...
        }
    }
}

重要なのは、各 .await ポイントがステートマシンの遷移に対応していることです。ステートマシンは await をまたぐローカル変数を保持します。これらの変数は、poll()Pending を返した後も、次の poll() 呼び出しまで存続し続ける必要があります。

Future トレイト

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

実行系(executor)は poll() をループで呼び出します。Future がまだ完了していない場合、Poll::Pending を返します。Pending を返す前に、Futurecx.waker() を通じて waker(コールバック関数)を登録する必要があります。この waker は、Future が準備可能になった場合(例:IO の完了、タイマーの期限切れ)に呼び出されます。実行系は wake を受け取ると、再度 poll() を実行します。

Pin の役割

ステートマシンの State1file_content: String を保持しています。もし Future が新しいメモリアドレスに移動(move)されると、そのアドレスを保持している内部ポインタ(存在する場合)がダングリングポインタになります。自己参照を持つ Futureselect! マクロや借用されたローカル変数を含むシナリオで一般的)の場合、Pin<&mut Self>Future が移動しないことを保証します。コンパイラは、pinned な値を移動させるために memcpy を生成しません。

単純な executor の骨格

fn block_on<F: Future>(mut future: F) -> F::Output {
    let waker = ...;                   // waker を作成
    let mut cx = Context::from_waker(&waker);
    let mut future = unsafe { Pin::new_unchecked(&mut future) };
    loop {
        match future.as_mut().poll(&mut cx) {
            Poll::Ready(v) => return v,
            Poll::Pending => std::thread::park(),  // wake を待機(本番コードでは epoll を使用)
        }
    }
}

これは概念的な説明に過ぎません。実際の executor(tokio, async-std など)は、ビジーウェイトではなく、IO 多重化のために epoll/kqueue/IOCP を使用します。

参考

  • Rust Book: 非同期処理の章(公式ドキュメントでは現在作成中)
  • Tokio tutorial: tokio.rs/tokio/tutorial
  • async-book: rust-lang.github.io/async-book

キーワード: async, await, Future, Pin, poll, state machine, executor, waker