このページの目次
async/await の内部メカニズム
async fnは、Futureトレイトを実装するステートマシンにコンパイラによって展開されます。各.awaitポイントは1つの状態です。PinはFutureがメモリ上で移動しないこと(自己参照構造)を保証し、Wakerは実行系(executor)に次のポーリングをいつ行えるかを伝えます。この展開プロセスを理解すれば、Rust 非同期処理の「ゼロコスト抽象」の仕組みと、Futureがなぜ安易に drop してはいけないかが理解できます。
async fn はコンパイル後にどうなるか
async
コンパイラは async fn を Future トレイトを実装する匿名型(ステートマシン)に変換します:
// コンパイラによって生成される擬似コード:
重要なのは、各 .await ポイントがステートマシンの遷移に対応していることです。ステートマシンは await をまたぐローカル変数を保持します。これらの変数は、poll() が Pending を返した後も、次の poll() 呼び出しまで存続し続ける必要があります。
Future トレイト
実行系(executor)は poll() をループで呼び出します。Future がまだ完了していない場合、Poll::Pending を返します。Pending を返す前に、Future は cx.waker() を通じて waker(コールバック関数)を登録する必要があります。この waker は、Future が準備可能になった場合(例:IO の完了、タイマーの期限切れ)に呼び出されます。実行系は wake を受け取ると、再度 poll() を実行します。
Pin の役割
ステートマシンの State1 は file_content: String を保持しています。もし Future が新しいメモリアドレスに移動(move)されると、そのアドレスを保持している内部ポインタ(存在する場合)がダングリングポインタになります。自己参照を持つ Future(select! マクロや借用されたローカル変数を含むシナリオで一般的)の場合、Pin<&mut Self> は Future が移動しないことを保証します。コンパイラは、pinned な値を移動させるために memcpy を生成しません。
単純な executor の骨格
これは概念的な説明に過ぎません。実際の 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