Rustの非同期プログラミングとTokio入門
上級編パート3開始:Futureトレイト、async/awaitによるステートマシン、Tokioランタイム、グリーンスレッドとOSスレッドの比較。
第3部:上級システム開発と実践Rust(Advanced Systems & Production Rust) へようこそ。数百万件の同時接続を処理するWebサーバーや分散システムを構築する際、接続ごとにOSスレッドを割り当てると、コンテキストスイッチの負荷とメモリ消費によりすぐにシステムが枯渇してしまいます。
Rustは、ゼロコスト抽象化に基づいた Async/Await モデルと、最高峰の非同期ランタイム Tokio を組み合わせることでこの課題を解決します。
graph TD
A["Rustの非同期モデル"] --> B["Future トレイト<br/>Lazy - pollされるまで実行されない"]
A --> C["async / .await<br/>ゼロアロケーションのステートマシン"]
A --> D["Tokio ランタイム<br/>Reactor + Work-stealing Executor"]
A --> E["tokio::spawn<br/>軽量タスク(数十〜数百バイト)"]
10歳でもわかる説明(ELI5):大繁盛のカフェ#
- 従来のマルチスレッド(OSスレッド):店員を100人雇い、各店員が1人のお客様の前につきっきりでコーヒーが淹れ終わるのを待ちます。莫大な人件費(RAM)がかかり、厨房がパンクします。
- Tokioによる非同期処理(Asyncタスク):凄腕のバリスタ4人だけで回します。注文を受けたら呼び出しベル(
Future)を渡し、コーヒーが抽出されている間、次の注文を受けます。コーヒーが完成したら(Poll::Ready)ベルが鳴り、即座に商品を渡します!
1. Rustの Future:デフォルトで遅延評価(Lazy)#
JavaScriptのPromiseと異なり、Rustの Future は 遅延評価(Lazy) です。.await でポーリングしない限り、一切コードは実行されません:
src/main.rs
// 非同期処理のコアトレイト
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}rustsequenceDiagram
autonumber
actor Executor as Tokio Executor
participant Task as 非同期タスク Future
participant Reactor as IO Reactor
Executor->>Task: poll(cx)
Task->>Reactor: I/O待機の登録(例: Socket読み込み)
Task-->>Executor: Poll::Pending を返却(スレッドを譲渡)
Note over Executor: ワーカースレッドは別タスクを実行
Reactor-->>Task: データ到着!cx.waker() でタスクを起床
Executor->>Task: 再度 poll(cx) を実行
Task-->>Executor: Poll::Ready(data) を返却して完了
async fn を記述すると、コンパイラはそれを自動的に ステートマシン(状態遷移列挙型) へ変換し、ヒープ確保を行わずにローカル変数の状態を中断・再開ポイント間で保持します。
2. Tokioランタイムの導入#
Rustは標準ライブラリに意図的に非同期ランタイムを含めていません。業界標準として最も利用されているのが Tokio です。
graph LR
subgraph TokioRuntime ["Tokio マルチスレッドランタイム"]
Reactor["I/O & タイマー Reactor<br/>(epoll / kqueue / IOCP)"]
W1["ワーカースレッド 1<br/>(タスクキュー)"]
W2["ワーカースレッド 2<br/>(タスクキュー)"]
W3["ワーカースレッド 3<br/>(タスクキュー)"]
end
W1 -.->|Work-stealing| W2
W2 -.->|Work-stealing| W3
Reactor -->|タスクを起床| W1
Cargo.toml に追加:
[dependencies]
tokio = { version = "1", features = ["full"] }toml#[tokio::main] による非同期プログラムの作成#
src/main.rs
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_user_data(user_id: u64) -> String {
println!("ユーザー {user_id} のデータを取得中...");
sleep(Duration::from_millis(500)).await; // ノンブロッキングスリープ
format!("ユーザー #{user_id} のデータ")
}
#[tokio::main]
async fn main() {
let result = fetch_user_data(42).await;
println!("結果: {result}");
}rust3. tokio::spawn による軽量タスクの生成#
tokio::spawn はTokioのスレッドプール上で実行される軽量非同期タスクを生成します:
src/main.rs
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let mut handles = vec![];
for i in 1..=5 {
let handle = tokio::spawn(async move {
sleep(Duration::from_millis(100 * i)).await;
println!("タスク #{i} 完了");
i * 10
});
handles.push(handle);
}
for handle in handles {
let res = handle.await.unwrap();
println!("受け取り結果: {res}");
}
}rust4. 並行コンビネータ:tokio::join! と tokio::select!#
tokio::join!: 複数のFutureを同時に待機#
src/main.rs
async fn get_temperature() -> i32 { 25 }
async fn get_humidity() -> i32 { 60 }
#[tokio::main]
async fn main() {
let (temp, hum) = tokio::join!(get_temperature(), get_humidity());
println!("気温: {temp}°C, 湿度: {hum}%");
}rusttokio::select!: 最初に完了した処理を採用#
src/main.rs
use tokio::time::{sleep, Duration};
async fn slow_computation() -> &'static str {
sleep(Duration::from_secs(2)).await;
"計算完了"
}
#[tokio::main]
async fn main() {
tokio::select! {
res = slow_computation() => {
println!("取得結果: {res}");
}
_ = sleep(Duration::from_secs(1)) => {
println!("タイムアウトしました!");
}
}
}rustOSスレッド vs Asyncタスク 比較表#
| 項目 | OSスレッド (std::thread) | Asyncタスク (tokio::spawn) |
|---|---|---|
| メモリ消費 | 約 1MB〜8MB / スレッド | 数十〜数百バイト / タスク |
| 最大生成数 | 数千程度 | 数百万単位 |
| コンテキストスイッチ | カーネルレベル(重い) | ユーザ空間のステート遷移(極めて軽量) |
| 最適用途 | CPUヘビーな計算処理 (CPU-bound) | ネットワーク通信、I/O待ち処理 (I/O-bound) |
まとめ#
- Rustの
Futureはポーリングされるまで動作しない遅延ステートマシンです。 - TokioはワークスティーリングExecutorと非同期I/Oを提供する標準ランタイムです。
- 大規模な並行処理には
tokio::spawnを使い、join!やselect!で制御フローを構築します。