Rust

Rustでasync fnをFutureへ変換する仕組みと自前Executor実装ガイド

ⓘ本ページはプロモーションが含まれています

スポンサードリンク

async fn がコンパイル時に生成する状態機械

何が起きているか

async fn呼び出し時に 実行されるコードではなく、コンパイル時に 以下の2つを自動で作ります。

  1. 匿名構造体 – 関数本体で使われるローカル変数や中間結果をフィールドとして保持。
  2. Future の実装 – その構造体が Future トレイトを実装し、poll メソッドの中で .await「現在の状態から次の状態へ遷移する」 ロジックに変換される。

参考: Rust Async Bookhttps://doc.rust-lang.org/book/ch20-02-multithreaded.html

cargo expand で見えるコード例

cargo expand(公式ツール)で得られる概形は次の通りです。

  • state が現在の制御フローを示す。
  • await は内部で別々の Futurepoll を呼び出し、Pending になった時点で外側の pollPending を返す。

この変換があるおかげで、ユーザーは .await と書くだけで非同期制御フローを記述できる。


基本的な async 関数と .await の使い方

  • task::sleep非同期タイマー で、内部的に OS のタイマーやイベントループを利用してブロックしない。
  • .await が現れるたびにコンパイラ生成コードの poll が呼ばれ、タイマーが完了すると再開される。

公式ドキュメント: https://doc.rust-lang.org/std/future/trait.Future.html


Future トレイトを手動で実装するポイント

3‑1️⃣ pollPinUnpin の意味

用語 説明
Pin<&mut T> T がメモリ上で移動しないことを保証。自己参照構造体(例: 内部に &self を保持する)を安全に扱える。
Unpin デフォルトではすべての型は Unpin だが、#[pin_project] 等で「ピン留めできない」構造体を作ることも可能。Future::poll の引数は Pin<&mut Self> なので、Unpin が必要なときは Box::pinPin::new_unchecked を使う。
安全にピン留めする方法
  • 手動で Pin::new(&mut value)(スタック上)
  • Box::pin(value)(ヒープ)
  • #[pin_project] クレートで自動生成された project() メソッドを利用

ピン留めの実例(pin_project 使用)


3‑2️⃣ カウンタ Future の実装例

  • ポイント: selfPin<&mut Self> で受け取っているが、この構造体は自己参照を持たないので Unpin でも問題なし。
  • 実際に使うときは futures::executor::block_on(SimpleCounter::new()) のように Pin::new が自動的に行われる。

3‑3️⃣ 正しいタイマー Future(ブロッキングしない実装)

以下の例では スレッドを生成してブロック する代わりに、std::thread::spawn非同期Waker を呼び出すだけです。ランタイム外でも安全に利用でき、ブロッキング操作は一切行いません。

  • 安全性: std::thread::sleep は別スレッドで実行され、ランタイムのスレッドをブロックしない。
  • 注意点: 実際のプロダクションコードでは tokio::time::Sleepasync‑std::task::sleep のように 非同期 I/O タイマー を利用する方がオーバーヘッドが低く、スレッド数を増やす必要がない。

公式ドキュメント: https://doc.rust-lang.org/std/future/trait.Future.html#method.poll


最小限の executor を作る

4‑1️⃣ RawWaker と安全な Waker の構築例

RawWaker低レベル API で、正しく実装しないと未定義動作になる。以下は所有権を Arc に委ねた完全版です。

  • cloneArc の参照カウントを増やすだけ。
  • wake / wake_by_ref では保存したクロージャを呼び出し、タスクキューへの再投入等の実装を行う。
  • drop は単に Arc を復元してスコープを抜けさせるだけで、参照カウントが減少する。

4‑2️⃣ futures::task::ArcWake とシングルスレッドキュー

futures クレートが提供する ArcWake は上記ロジックを簡潔に書くヘルパーです。以下は ミニ executor の核心部分です。


4‑3️⃣ 実行ループ(シングルスレッド)

ポイント
- Waker が呼び出された瞬間に同スレッド上で poll が再度実行できるようになる。
- 本コードは シングルスレッド 向けなので、Mutex だけで競合を防げます。マルチスレッド化する場合は Arc<Mutex<_>> の代わりに crossbeam::queue::SegQueue 等を検討してください。


カスタムランタイムと既存ライブラリの比較・実務での選択基準

項目 Tokio (公式) async‑std (公式) 本記事のミニ executor
スレッドモデル ワーカープール(デフォルト=CPU 数) シングルスレッドまたは task::block_on 手動実装(シングル or カスタム)
I/O サポート 完全非同期 TCP/UDP/ファイル、TLS 等多数 同様に非同期 I/O が標準装備 外部クレート (mio, tokio‑io) を自前で組み合わせる必要あり
エコシステム #[tokio::main] マクロ、豊富なプラグイン 標準ライブラリに近い API 学習・プロトタイプ向き
オーバーヘッド 高め(スケジューラ最適化が多い) 中程度 最小限実装なら極低(ただし機能は限定的)
推奨シーン 大規模サーバ、マルチコア活用が必須 CLI ツール、軽量サービス ランタイム内部で独自スケジューリングが必要なライブラリ開発や教育目的

どちらを選ぶべきか?

条件 推奨
高スループット・マルチコアが必須 Tokio
依存を極力減らしたい、シンプルさ重視 async‑std
ランタイムの内部構造を自分で制御したい、または学習目的 本ミニ executor

公式情報: https://tokio.rs/ , https://docs.rs/async-std/latest/async_std/


典型的なコンパイルエラーと対処法

エラーメッセージ例 原因 修正例
future cannot be sent between threads safely FutureSend を実装していない(スレッド間で共有) すべての内部型を Arc<Mutex<_>> + Send に置き換える。例: Box::pin(async move { … })
cannot move out of captured outer variable in an async block 所有権を async ブロック内で移動しようとした 変数を Arc::clone(&var) して共有、または move キーワードを外す
the trait bound 'MyFuture: Unpin' is not satisfied Pin<&mut Self> が必要なのに Unpin がない Box::pin(my_future) でピン留め、もしくは #[pin_project] を使う

実践的な対処フロー

  1. エラーメッセージを読む → 必要なトレイト (Send, Sync, Unpin) が足りないことが多い。
  2. 型の所有権・共有方法を見直すArc/Mutex の組み合わせでスレッド安全にする。
  3. ピン留めが必要か判定 → 自己参照構造体なら必ず Pin が必要。pin_project を導入すると安全にフィールドごとに #[pin] を付与できる。

デバッグ・ベンチマーク手法

1. cargo expand

  • 目的: async fn がどのような構造体・poll に変換されたかを可視化。

2. tokio::task::yield_now / async_std::task::yield_now

  • 活用例: 手動でスケジューラの切り替えタイミングを観測し、デッドロックやスタックオーバーフローを防止。

3. criterion でベンチマーク

  • 比較対象: SimpleCounter(自前実装) vs. tokio::time::sleep 等。
  • 結果の活用: どちらがレイテンシ/スループットに優れるかを数値で示す。

4. ロギングとトレーシング

クレート 用途
tracing 非同期タスクの開始・終了を階層的に可視化。
env_logger 手軽なログ出力(RUST_LOG=debug cargo run)。


次のステップ

  1. コードをローカルにコピーcargo new async_demo && cd async_demo
  2. 必要な依存 (futures, lazy_static, pin-project) を Cargo.toml に追記。
  3. cargo run でミニ executor がタスクを正しくスケジュールできるか確認。
  4. 完成したら GitHub にリポジトリを作り、README に本記事へのリンクと実行手順を書いて公開。
  5. 定期的に 公式ドキュメント(Rust Async Book、Tokio Docs)やクレートの CHANGELOG をチェックし、最新機能やベストプラクティスを取り入れる。

公式情報まとめ
- Rust async/await: https://doc.rust-lang.org/std/future/
- Tokio: https://tokio.rs/tokio/tutorial
- futures crate: https://docs.rs/futures/latest/futures/


この記事は、冗長な「Point (再提示)」を削除し、実装例の正確性と安全性に重点を置いた構成になっています。読者が自分で非同期コードを書き、必要に応じてカスタムランタイムまで作れるようになることを目指しています。

スポンサードリンク

-Rust
-, , , , , ,