blog.dopana

Back

第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,
}
rust
sequenceDiagram
    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}");
}
rust

3. tokio::spawn による軽量タスクの生成#

tokio::spawn はTokioのスレッドプール上で実行される軽量非同期タスクを生成します:

4. 並行コンビネータ: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}%");
}
rust

tokio::select!: 最初に完了した処理を採用#

OSスレッド 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! で制御フローを構築します。

参考文献#