Rust Tokio「Cannot start a runtime from within a runtime」エラーを非同期コンテキストでblock_onを呼び出す際に修正する方法

intermediate🦀 Rust2026-07-10| Rust 1.60+、Tokio 1.x、Linux/macOS/Windows — Tokio非同期ランタイムを実行するすべてのプラットフォーム

Error Message

thread 'main' panicked at 'Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.'
#tokio#async#runtime#block_on#nested-runtime

TL;DR

TokioはRuntimeのネストを許可しません。Tokioのスレッド上で既に実行中の非同期関数の内部からblock_on()を呼び出すと、即座にパニックが発生します。適切な修正方法は、問題のあるコードの所有者によって異なります:

  • 非同期コードを自分で所有している場合: block_on(future)future.awaitに置き換えるだけで完了です。
  • 内部でblock_onをラップしている同期ライブラリを呼び出している場合: 呼び出しをtokio::task::spawn_blockingまたはtokio::task::block_in_placeの中に移動してください。
  • 完全なRuntime分離が必要な場合: 実際のOSスレッドを生成し、独自のRuntime::new()を持たせてください。

実際に何が起きているのか

Tokioのスレッドプールのスレッドはランタイムに所有されています。これらのスレッドの1つからHandle::block_on()またはRuntime::block_on()を呼び出すと、Tokioは意図的にパニックを起こします。Futureをポーリング中のスレッドをブロックすると、エグゼキュータが即座にデッドロックするためです。

主に以下の3つの状況でこの問題が発生します:

  • サードパーティライブラリが内部でblock_onを呼び出している。非同期HTTPクライアントのSDKラッパーが典型的な原因です。
  • 同期ヘルパーがローカルのRuntimeを構築してblock_onを呼び出し、そのヘルパーをasync fnの内部から呼び出している。
  • main()#[tokio::main]を付けているにもかかわらず、本文のどこかでtokio::runtime::Builder::new_current_thread().block_on()も試みている。
// パニック — #[tokio::main]コンテキスト内でblock_onを呼び出している
#[tokio::main]
async fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let result = rt.block_on(some_async_fn()); // ここでPANIC
    println!("{:?}", result);
}

修正1:.awaitを使用する(推奨のデフォルト)

コードを自分で制御できる場合は、block_onを完全に削除し、.awaitに任せましょう。

// 修正前(パニックが発生する)
fn fetch_data() -> String {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async_fetch()) // 非同期コンテキスト内でパニック
}

// 修正後(正しい)
async fn fetch_data() -> String {
    async_fetch().await
}

呼び出し元のコードはこうなります:

#[tokio::main]
async fn main() {
    let data = fetch_data().await;
    println!("{}", data);
}

これがほぼ常に正しい答えです。ライブラリの制約により選択肢がない場合にのみ、他の修正方法を検討してください。

修正2:同期処理やCPUバウンド処理にはspawn_blockingを使用する

ブロックしなければならない同期関数がある場合(レガシーSDK、CPUヘビーな計算処理、同期ファイルI/Oなど)、spawn_blockingを使ってTokioの専用ブロッキングスレッドプールに移動させましょう。これらのスレッドは非同期エグゼキュータとは別に管理されるため、パニックもデッドロックも発生しません。

#[tokio::main]
async fn main() {
    let result = tokio::task::spawn_blocking(|| {
        // ここでのブロックは安全 — 専用のブロッキングスレッドで実行される
        legacy_sync_client::fetch()
    })
    .await
    .unwrap();

    println!("{:?}", result);
}

Tokioはデフォルトでブロッキングスレッドを512個に制限しています。散発的な呼び出しには十分な数です。継続的な高負荷下で数百のスレッドを生成する場合は、本番環境で問題が発生する前にBuilder::max_blocking_threads()で上限を調整してください。

修正3:インラインブロッキングにはblock_in_placeを使用する

別スレッドにオフロードするのではなく、block_in_placeは他のタスクを現在のスレッドから移動させ、その場でブロックできるようにします。spawn_blockingよりも配線が簡単ですが、マルチスレッドランタイムでのみ動作します。

#[tokio::main] // デフォルト = multi_thread
async fn main() {
    let result = tokio::task::block_in_place(|| {
        // インラインのブロッキング呼び出し — ランタイムが他のタスクをこのスレッドから移動させる
        some_blocking_sdk::run()
    });
    println!("{:?}", result);
}

current_threadで実行している場合は、代わりにspawn_blockingを使用してください。block_in_placeはシングルスレッドのランタイムではパニックが発生します。タスクを移動させる他のスレッドが存在しないためです。

修正4:独自のRuntimeを持つ別OSスレッドを使用する

ライブラリが内部でblock_onを呼び出しており、変更が本当にできない場合があります。回避策として、Tokioのスレッド上でそれらを実行しないようにします。エグゼキュータとは完全に独立した実際のOSスレッドを生成し、そこで新しいランタイムを構築します。

#[tokio::main]
async fn main() {
    // TokioのタスクではなくOSの真のスレッド
    let handle = std::thread::spawn(|| {
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            // 安全 — Tokioが所有しないスレッド上の新しいランタイム
            some_library_with_internal_block_on()
        })
    });

    let result = handle.join().unwrap();
    println!("{:?}", result);
}

少々大掛かりな方法ですが、依存ライブラリがblock_onをハードコードしており変更できない場合、これが最も確実な解決策です。

どの修正が必要かを診断する

パニックのスタックトレースを確認してください。原因が直接示されています:

thread 'tokio-runtime-worker' panicked at 'Cannot start a runtime from within a runtime.
...
stack backtrace:
   0: tokio::runtime::scheduler::block_on
   1: some_sdk::client::Client::send    {
            // すでにTokio内にいる — 呼び出し元はdo_work_async().awaitを使用する必要がある
            panic!("do_work()は非同期コンテキストから呼び出せません。do_work_async().awaitを使用してください");
        }
        Err(_) => {
            // ランタイムが存在しない — 新しいランタイムの作成は安全
            let rt = tokio::runtime::Runtime::new().unwrap();
            rt.block_on(internal_async_work());
        }
    }
}

修正の確認

バイナリを実行してパニックが解消されていることを確認してください:

cargo run 2>&1 | grep -i "cannot start a runtime"
# 出力なし = 修正済み

リグレッションを早期に検出するための統合テストを追加してください:

#[tokio::test]
async fn test_no_nested_runtime_panic() {
    // パニックが発生してはならない
    let result = fetch_data().await;
    assert!(!result.is_empty());
}

実際の負荷下でspawn_blockingを使用している場合は、スレッド数に注意してください。デフォルトの512スレッド上限は十分に思えますが、継続的なブロッキング呼び出しは急速に積み重なります。本番環境で上限に達する前に、Builder::max_blocking_threads()で使用状況を確認してください。

クイックリファレンス

  • .await — コードを自分で所有している場合の第一選択
  • spawn_blocking — ブロッキングスレッド上での同期コード;マルチスレッドとcurrent_threadランタイムの両方で動作
  • block_in_place — インラインブロッキング;マルチスレッドランタイムのみ対応
  • std::thread::spawn + 新しいRuntime — 依存ライブラリがblock_onを強制し変更できない場合

Related Error Notes