解決策:tokio::spawn での Rust エラー 'future cannot be sent between threads safely'

intermediate🦀 Rust2026-04-22| Rust (全バージョン), Tokio 実行環境, マルチスレッドエグゼキュータ

Error Message

error[E0277]: future cannot be sent between threads safely, the trait `Send` is not implemented
#rust#非同期#tokio#Send#Future#スレッド

エラーメッセージ

Rustのコンパイラは非常に親切なことで有名ですが、エラーE0277は、初めて遭遇したときには立ちはだかる壁のように感じられることがあります。おそらくバックグラウンドタスクを生成しようとしたところ、future is not Send という赤いテキストの壁に突き当たったのでしょう。通常、以下のようなエラーが表示されます:

error[E0277]: future cannot be sent between threads safely
   --> src/main.rs:10:5
    |
10  |     tokio::spawn(async move {
    |     ^^^^^^^^^^^^ future returned by `async` block is not `Send` 
    |
    = help: the trait `Send` is not implemented for `std::sync::MutexGuard<i32>`
    = note: future is not `Send` as this value is used across an await

なぜこれが発生するのか

Tokioのデフォルトのスケジューラは、マルチスレッドのワークスティーリング(work-stealer)方式です。CPUコアを効率的に使用するために、タスクをワーカースレッドのプール(通常は合計コア数に一致)の間で移動させます。タスクはスレッド1で開始され、.await ポイントに到達した後、データが準備できたときにスレッド4で再開される可能性があります。

この「スレッドのホッピング」は高性能ですが、厳しい要件を生みます。それは、.await を跨いで保持されるすべてのデータが Send トレイトを実装していなければならないということです。タスクが一時停止している間にスレッドセーフでない型を保持している場合、コンパイラはデータレースの発生を防ぐために実行を停止させます。

ほとんどの開発者がこの壁に突き当たる理由は、主に2つあります:

  • ロックの競合: .await を呼び出している間、標準の std::sync::MutexGuard を保持している。
  • 不適切なプリミティブ: 非同期ブロック内で RcRefCell のようなシングルスレッド用の型を使用している。

解決策1:ガードを早めにドロップする

std::sync::MutexGuard は、スレッド間で転送できないOSレベルのスレッドIDに依存しているため、Send ではありません。ロジックが短い更新のためにロックを必要とするだけなら、.await に到達する前にガードが破棄されるようにしてください。

誤ったコード:

use std::sync::Mutex;

async fn problematic_task(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        let mut guard = data.lock().unwrap();
        *guard += 1;
        
        // この await はスレッドを跨いでガードを生存させ続けます!
        some_async_function().await; 
        
        println!("Value: {}", *guard);
    });
}

修正後のコード:

use std::sync::Mutex;

async fn fixed_task(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        {
            let mut guard = data.lock().unwrap();
            *guard += 1;
        } // ここでガードがドロップされ、タスクがスレッドを移動できるようになります

        some_async_function().await;
    });
}

ミューテックスのロジックを単純なブロック { ... } で囲むことで、MutexGuard がスコープ外に出ることを保証します。これは、より複雑な非同期ロックのオーバーヘッドを回避できるため、最も効率的な修正方法です。

解決策2:TokioのMutexに切り替える

APIレスポンスやデータベースクエリを待っている間、実際にロックを保持し続ける必要がある場合があります。このような場合は、標準ライブラリの Mutex を tokio::sync::Mutex に置き換えてください。そのガードは、特に Send になるように設計されています。

実装例:

use tokio::sync::Mutex;
use std::sync::Arc;

async fn holding_lock_across_await(data: Arc<Mutex<i32>>) {
    tokio::spawn(async move {
        // 注意: .lock() は非同期呼び出しになります
        let mut guard = data.lock().await; 
        
        // tokio::sync::MutexGuard は Send を実装しているため、これは安全です
        some_async_function().await;
        
        *guard += 1;
    });
}

これの使用は控えめにしてください。非同期関数の戻りに時間がかかる場合、.await を跨いでロックを保持するとアプリケーションのボトルネックになる可能性があります。

解決策3:RcをArcに交換する

エラーメッセージに std::rc::Rc が含まれている場合、アトミックではない参照カウンタを使用しています。Rc はシングルスレッドの作業では高速ですが、スレッドプールに必要な内部同期が欠けています。代わりに Arc (Atomic Reference Counted) を使用する必要があります。

誤ったコード:

let shared_val = Rc::new(10);
tokio::spawn(async move {
    println!("{}", shared_val); // Rc は Send ではありません
});

修正後のコード:

let shared_val = Arc::new(10);
tokio::spawn(async move {
    println!("{}", shared_val); // Arc はスレッドセーフであり Send です
});

検証ステップ

コンパイラが満足しているか確認するために、クイックチェックを実行します:

cargo check

エラーが解消されない場合は、出力の「note」セクションを詳しく確認してください。多くの場合、Send ではない変数が作成された正確な行が示されます。「this value is used across an await」といったフレーズを探して、原因を特定してください。

ベストプラクティス

  • 早めにチェックする: 何百行もコードを書くのを待つのではなく、頻繁に cargo check を実行してください。
  • セクションを小さく保つ: 可能な限り、クリティカルセクションを数行のコードに制限してください。
  • Avoid RefCell: スレッドを跨いで内部可変性が必要な場合は、Mutex または RwLock を使用してください。
  • カスタムトレイトを確認する: タスクにカスタムトレイトが含まれる場合は、Tokioがワーカー間で移動できるように、+ Send バウンドがあることを確認してください。

Related Error Notes