Contents
導入:目的・想定読者・進め方(ハンズオン準備)
このガイドはRustのasync/awaitを実務で使うための入門です。
Futureの内部動作とランタイム選定を学び、実装・運用上の注意点を押さえます。
付属サンプルを手元で実行しながら段階的に学べる構成です。
目的とゴール
ここで得られることを短く示します。
- async/awaitとFutureのポーリングモデルを実務観点で理解できます。
- Tokioやasync-std等のランタイム差を踏まえた設計判断ができます。
- テスト・可観測性・TLS運用など運用面の必須チェックリストを得られます。
想定読者・前提条件
対象読者と前提を明確にします。
- Rustの所有権と型システムが分かる初〜中級エンジニア向けです。
- Cargoでビルドやテストができる環境を想定します。
- nightly固有APIやunsafeを安易に使う前提ではありません。
進め方(ハンズオン準備)
実際に動かす手順の概要を示します。
- 付属のサンプルリポジトリをクローンしてREADMEに従ってください。
- ランタイム切替用のfeatureでTokio/async-stdを切り替えられます。
- この記事では実行例(cargoコマンド)を随所に示します。
Futureの内部動作とWaker
Futureは単なる値ではなく、状態を持つ「ポーリングされる」計算です。
PollとWakerの挙動を理解すると、停止や再開の原因が追いやすくなります。
ここでは概念説明と最小限のコード例を示します。
poll と Waker の基本
pollの役割とWakerの働きを簡潔に説明します。
- ランタイムはFuture::pollを繰り返し呼びます。
- Poll::Pendingを返すとタスクは停止します。
- Wakerを呼ぶとランタイムが再度pollします。
- poll内でブロッキングしてはいけません。ランタイムのスレッドを塞ぎます。
手動での Future 実装(最小例)
手動実装は稀ですが、理解に有用です。次は骨子の例です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; struct MyFuture { /* 状態 */ } impl Future for MyFuture { type Output = i32; fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> { // 状態に応じて Poll::Pending / Poll::Ready を返す Poll::Ready(42) } } |
最小の実行例(await の実行方法)
async関数を実行する代表的な方法を示します。実行コマンドも合わせて示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// src/main.rs #[cfg(feature = "tokio-runtime")] #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main() { run().await; } #[cfg(feature = "async-std-runtime")] fn main() { async_std::task::block_on(run()); } async fn run() { println!("Hello async"); } |
- Tokioで実行: cargo run --features tokio-runtime
- async-stdで実行: cargo run --features async-std-runtime
これにより、READMEだけでなく記事内で即実行できる手順を示しました。
Pin/Unpin と所有権・ライフタイムの実務的影響
Pinは値のメモリ位置固定を扱う仕組みです。
自己参照やawait跨ぎの借用が原因で生じるエラーへの対処が重要です。
ここでは典型的な問題と実務的な回避策を示します。
Pin の基本と Box::pin の使い所
Pinで固定するパターンを説明します。
- 自己参照を含む型は移動で参照が無効化します。
- Box::pinでヒープ確保して位置を固定できます。
- 実務ではpin-projectなどを使うか設計で自己参照を避けます。
|
1 2 3 4 5 |
use std::pin::Pin; let fut = Box::pin(MyFuture { /* state */ }); // futはここから移動できないため、poll実装と安全に相互作用できます |
await を跨ぐ借用エラーの典型例と対処
await跨ぎで借用が問題になる理由と回避策です。
- 問題例: 参照を作ってからawaitし、参照先が移動してしまう。
- 回避1: スコープを短くし参照を先に使い切る。
- 回避2: clone/Arcで所有権を分離する。
- 回避3: 値をmoveしてawait内で所有する。
例(bad → good):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// NG: rがawaitを跨いで生きる async fn ng() { let s = String::from("hi"); let r = &s; some_async().await; // rの有効期間が問題になる println!("{}", r); } // OK: スコープを区切る async fn ok() { { let s = String::from("hi"); let r = &s; println!("{}", r); } some_async().await; } |
よくあるコンパイルエラーと実務的対処
典型的エラーと短い解決策を示します。
- borrow error(borrowed value does not live long enough)
- 対処: スコープを短くする、clone/Arcを使う、所有権を移す。
- futureがSendでないためspawnできない
- 対処: ArcでSend化、LocalSetでローカル実行、または設計を見直す。
- Pin関連のエラー(自己参照)
- 対処: Box::pinで固定、pin-projectを使用、または自己参照を避ける。
- std::sync::Mutexをawait越しに保持してハングする
- 対処: tokio::sync::Mutex等の非同期対応プリミティブを使う。
- 補足: std::sync::MutexはOSスレッドをブロックします。単一スレッドexecutor上でブロッキングすると全タスクが止まります。
ランタイムの役割と具体的な切替手順
ランタイムはExecutorとReactorを提供し実行モデルを決めます。
選定ミスは依存ライブラリの不整合やパフォーマンス問題を招きます。
ここでは比較と具体的な切替手順を示します。
主要ランタイムの比較
主要ランタイムの特徴を簡潔に比較します。
| ランタイム | 特徴 | 向くケース | 注意点 |
|---|---|---|---|
| Tokio | エコシステムが広く機能豊富 | 大規模サービス、高性能 | 機能が多く学習コストあり |
| async-std | std風APIで学習容易 | シンプルな非同期処理 | Tokio中心のクレートと互換性に注意 |
| smol | 軽量で小さなフットプリント | 組込みやバイナリサイズ重視 | エコシステムが小さい |
ランタイム間互換性の具体的課題
実務で遭遇する互換性の問題を列挙します。
- 型の不一致(tokio::net::TcpStream と async-std::net::TcpStream)
- I/Oの登録やReactor実装の差により低レイヤが非互換になる場合がある
- 一部クレートが内部で特定ランタイムのランタイムハンドルを参照する
- 対策: プロセス分離、互換レイヤ(async-compat 等)、境界での同期化
実行例とランタイム切替の手順
ランタイム切替の典型的なCargo設定と実行例です。
|
1 2 3 4 5 6 7 8 9 10 |
# Cargo.toml(抜粋) [features] default = ["tokio-runtime"] tokio-runtime = ["tokio"] async-std-runtime = ["async-std"] [dependencies] tokio = { version = "1", optional = true, features = ["full"] } async-std = { version = "1", optional = true } |
|
1 2 3 4 5 6 |
# Tokioでビルド/実行 cargo run --features tokio-runtime # async-stdでビルド/実行 cargo run --features async-std-runtime |
ランタイムを明示することで、依存クレートのfeatureや動作を制御できます。
実務パターンとライブラリ選定
実装パターンと主要ライブラリの使い分けを示します。
設計上の注意点を合わせて記載します。
Tokioの実務ポイント
Tokioでよく使う注意点をまとめます。
- サーバーは通常multi-thread型で動かします。Builderでスレッド数を調整してください。
- tokio::spawnはSend + 'staticなFutureを期待します。非Sendはspawn_localかLocalSetで扱います。
- ブロッキング処理は短時間ならtokio::task::spawn_blockingで扱います。長時間CPU処理は専用ワーカーへ分離してください。
- タイムアウトは tokio::time::timeout を使い、必要ならクリーンアップ処理を行います。
|
1 2 3 |
let res = tokio::time::timeout(Duration::from_secs(5), some_io()).await; let heavy = tokio::task::spawn_blocking(|| heavy_work()).await?; |
ライブラリ選定(HTTP / gRPC / DB)
代表的な選択肢と用途を示します。
- HTTPクライアント: reqwest(高レベル)
- HTTPサーバー: axum(tower基盤)、hyper(低レイヤ)
- gRPC: tonic(ストリーミングやデッドライン対応)
- DB: sqlx(compile-timeチェック機能あり)、tokio-postgres(低レイヤ)
sqlxのコンパイル時クエリチェックを使う場合は、macros等のfeatureとビルド時の環境変数が必要です。例:
|
1 2 3 |
# Cargo.toml(抜粋) sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "macros"] } |
|
1 2 3 4 5 |
export DATABASE_URL=postgres://user:pass@localhost/db # sqlx-cliを使って準備する例 cargo install sqlx-cli --no-default-features --features postgres,rustls cargo sqlx prepare |
バージョンやfeatureは各crateのドキュメントで必ず確認してください。
非同期パターン:join/select/stream
代表的パターンと短い例を示します。
- 並列実行: tokio::join!
- レースやタイムアウト: tokio::select!
- イベント逐次処理: futures::stream と for_each_concurrent
|
1 2 3 4 5 6 7 |
let (a, b) = tokio::join!(f1(), f2()); tokio::select! { res = fut1 => { /* fut1の処理 */ } _ = tokio::time::sleep(Duration::from_secs(1)) => { /* タイムアウト */ } } |
テスト・デバッグ・パフォーマンス最適化・運用チェックリスト
品質確保と運用性は非同期システムで特に重要です。
ここではテスト、可観測性、最適化、TLSを含む運用チェックリストを実務的にまとめます。
テストとCI
非同期コードのテスト方法とCI方針を示します。
- 非同期単体テストは#[tokio::test]や#[async_std::test]を利用します。
- 時間依存のテストはタイムモックを使って決定論的にします。
- CIではreleaseビルド、依存脆弱性スキャン、feature組合せ検証を組み込みます。
|
1 2 3 4 5 6 |
#[tokio::test] async fn async_it_works() { let r = my_async_fn().await; assert_eq!(r, 42); } |
可観測性とツール
推奨ツールと最小設定の例を示します。
- tracing: 構造化ログとスパンを使います。tracing_subscriberで初期化します。
- tokio-console: タスク状況やブロッキングを可視化します。console-subscriberを導入して初期化してください。
- メトリクス: Prometheus互換のexporterを用いてレイテンシとスループットを収集します。
- プロファイリング: samplingプロファイラでCPU・メモリのボトルネックを特定します。
初期化の最小例:
|
1 2 3 4 5 6 |
let subscriber = tracing_subscriber::FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber).unwrap(); // tokio-consoleの例(console-subscriberを追加しinitする) console_subscriber::init(); |
ツールによって追加のfeature有効化や環境設定が必要です。公式ドキュメントを参照してください。
運用チェックリスト(TLS/セキュリティ)
TLSと運用の必須項目をチェックリスト化します。
- サポートTLSバージョンを明確にする(TLS1.2以上、TLS1.3推奨)。
- 推奨暗号スイート: TLS1.3のAES-GCM系とChaCha20-Poly1305を優先。TLS1.2ではECDHE+AESGCMを採用。
- 証明書ローテーション: 期限切れ監視、事前通知、自動更新(ACME等)を設定する。
- OCSP/CRL: サーバでのOCSP staplingを有効化し、失効確認手順を運用する。
- 鍵管理: 秘密鍵はKMSやVault等で安全に管理する。
- 証明書の無停止リロード手順を用意する。事前検証とロールバック手順を整備する。
- TLS設定の定期テスト: SSL Labsや内部スキャンで設定を検証する。
- ライブラリ選択: rustlsはRustネイティブで安全性とメンテナンス性が高い選択肢です。必要に応じてOpenSSLとの互換性も検討する。
- 設定例のCI検証: テスト環境で期限切れや失効のシミュレーションを行う。
トラブルシューティングの手順
問題発生時の手順を段階的に示します。
- 最小再現ケースを作る。
- tracingでログとスパンを増やす。
- tokio-consoleでタスク状態を観察する。
- プロファイラでCPU/メモリを計測する。
- 段階的に構成を絞って原因を特定する。
公式ガイドライン向けの注記(サポート範囲・責任)
運用ドキュメント化する際に明記すべき事項を示します。
- サポートするランタイムとバージョンを明示すること。
- 提供範囲(コード例、設定例)はサポート対象外の環境では動作保証しない旨を記載すること。
- セキュリティに関する最終責任は運用側にある旨を明示すること。
- 重要な暗号設定や証明書管理は環境ごとに検証が必要である旨を追記すること。
参考リソース(入門→応用→ツール別の推奨参照箇所)
学びを深めるための主要ドキュメントとツールをカテゴリ別に示します。必ず公式ドキュメントで最新版を確認してください。
入門・基礎
- Rust Async Book(公式): https://rust-lang.github.io/async-book/
- Tokio公式: https://tokio.rs/
- async-std公式: https://async.rs/
応用(フレームワーク・DB・gRPC)
- axum: https://docs.rs/axum/
- hyper: https://hyper.rs/
- reqwest: https://docs.rs/reqwest/
- tonic (gRPC): https://github.com/hyperium/tonic
- sqlx: https://github.com/launchbadge/sqlx
- tokio-postgres: https://docs.rs/tokio-postgres/
ツール・CI・セキュリティ
- tracing: https://docs.rs/tracing/
- tokio-console: https://github.com/tokio-rs/console
- cargo-audit(脆弱性検出): https://github.com/RustSec/cargo-audit
- cargo-deny(依存検査): https://github.com/EmbarkStudios/cargo-deny
これらを入門→応用→ツールの順で参照すると学習効率が高まります。
まとめ
- FutureはポーリングとWakerで再開される状態マシンです。理解がデバッグ力を高めます。
- Pinやawait跨ぎの借用を設計段階で整理し、async対応プリミティブを使ってください。
- ランタイム選定は依存クレートと運用要件で決定し、切替はCargoのfeatureで管理できます。
- sqlxやtokio-console等はfeatureや環境設定が必要です。公式ドキュメントの手順に従ってください。
- TLS運用は証明書ローテーション、失効確認、推奨暗号スイートを含むチェックリストで管理してください。