あっぽログ
← 記事一覧に戻る

RustのTokioでタスクスポーンを使いこなす:spawn・JoinHandle・タスク管理の基本

はじめに

Rustで非同期プログラミングを行う際、tokio::spawnはとても強力な道具です。バックグラウンドで複数の処理を同時に走らせたいとき、スポーンを使えばタスクを独立した単位として扱えます。

この記事では、tokio::spawnの基本的な使い方から、JoinHandleによる結果の取得、複数タスクの管理方法までを順を追って解説します。


tokio::spawnとは

tokio::spawnは、非同期タスクをTokioのランタイム上で独立して実行するための関数です。OSのスレッドとは異なり、非常に軽量で、数千〜数万のタスクを同時に起動しても問題ありません。

use tokio;

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("バックグラウンドタスクが動いています");
    });

    // タスクの完了を待つ
    handle.await.unwrap();
    println!("メインタスク終了");
}

tokio::spawnに渡す引数はasyncブロックで、JoinHandleが返り値として得られます。


JoinHandleで結果を取得する

spawnの戻り値であるJoinHandle<T>awaitすることで、タスクの戻り値を取得できます。

use tokio;

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 何らかの計算結果を返す
        42u32
    });

    match handle.await {
        Ok(value) => println!("タスクの結果: {}", value),
        Err(e) => eprintln!("タスクがパニックしました: {:?}", e),
    }
}

JoinHandle::awaitResult<T, JoinError>を返します。タスクがパニックした場合はErrになるため、unwrap()よりmatch?演算子で丁寧に扱うのがおすすめです。


複数タスクを並列実行する

実践的なシーンでは、複数のタスクを一斉に起動して全部の完了を待ちたいことがよくあります。

use tokio;

async fn fetch_data(id: u32) -> String {
    // 本来はHTTPリクエストなどが入る
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    format!("データ#{}", id)
}

#[tokio::main]
async fn main() {
    let mut handles = vec![];

    for i in 1..=5 {
        let handle = tokio::spawn(async move {
            fetch_data(i).await
        });
        handles.push(handle);
    }

    for handle in handles {
        match handle.await {
            Ok(result) => println!("{}", result),
            Err(e) => eprintln!("エラー: {:?}", e),
        }
    }
}

async moveとキャプチャ

上記の例でasync moveを使っている点に注目してください。spawnに渡すブロックは'staticライフタイムを要求するため、外部変数を参照ではなく所有権ごと移動する必要があります。moveキーワードで変数をブロック内に取り込むのが一般的なパターンです。


タスクをキャンセルする

JoinHandleにはabort()メソッドがあり、実行中のタスクを外部からキャンセルできます。

use tokio;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("タスク開始");
        sleep(Duration::from_secs(10)).await;
        println!("タスク完了(ここには到達しない)");
    });

    // 少し待ってからキャンセル
    sleep(Duration::from_millis(50)).await;
    handle.abort();

    match handle.await {
        Ok(_) => println!("正常終了"),
        Err(e) if e.is_cancelled() => println!("タスクはキャンセルされました"),
        Err(e) => eprintln!("その他のエラー: {:?}", e),
    }
}

JoinError::is_cancelled()でキャンセルによる終了かどうかを判定できます。


tokio::joinマクロとの使い分け

少数の固定タスクを並列実行するだけなら、tokio::join!マクロも便利です。

#[tokio::main]
async fn main() {
    let (a, b) = tokio::join!(
        async { 1u32 + 1 },
        async { 2u32 * 3 }
    );
    println!("a={}, b={}", a, b);
}
手法向いているケース
tokio::spawn動的に数が変わるタスク、独立して管理したいタスク
tokio::join!固定数のタスクをまとめて待つだけのケース

まとめ

  • tokio::spawnで非同期タスクを独立して起動できる
  • 戻り値のJoinHandleawaitすることで結果を取得できる
  • 複数タスクをVec<JoinHandle>で管理して一括処理できる
  • async moveで外部変数を所有権ごとタスクに渡す
  • abort()でタスクを途中でキャンセルできる

Tokioのスポーン機能を使いこなすと、複数のI/O処理や重い計算を並行して走らせる柔軟な設計が可能になります。ぜひ自分のプロジェクトで試してみてください。

← 記事一覧に戻る