エグゼキュータをブロックする

ほとんどの async ランタイムでは、並行して実行できるのは I/O タスクだけです。これは、CPU をブロックするタスクがエグゼキュータをブロックし、ほかのタスクの実行を妨げることを意味します。簡単な回避策は、可能な場合は async 相当のメソッドを使うことです。

// 著作権 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0

use futures::future::join_all;
use std::time::Instant;

async fn sleep_ms(start: &Instant, id: u64, duration_ms: u64) {
    std::thread::sleep(std::time::Duration::from_millis(duration_ms));
    println!(
        "future {id} slept for {duration_ms}ms, finished after {}ms",
        start.elapsed().as_millis()
    );
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let start = Instant::now();
    let sleep_futures = (1..=10).map(|t| sleep_ms(&start, t, t * 10));
    join_all(sleep_futures).await;
}
  • コードを実行し、sleep が並行ではなく連続して発生することを確認してください。

  • "current_thread" フレーバーでは、すべてのタスクが単一のスレッドに配置されます。これによりこの効果はより分かりやすくなりますが、このバグはマルチスレッドのフレーバーでも依然として存在します。

  • std::thread::sleeptokio::time::sleep に置き換え、その結果を await してください。

  • 別の修正方法としては tokio::task::spawn_blocking を使うことです。これは実際のスレッドを生成し、そのハンドルをエグゼキュータをブロックすることなく future に変換します。

  • タスクを OS スレッドとして考えるべきではありません。両者は 1 対 1 には対応せず、エグゼキュータでは複数のタスクを単一の OS スレッド上で実行できます。これは、FFI 経由で他のライブラリとやり取りする場合に特に問題になります。そのライブラリがスレッドローカルストレージに依存していたり、特定の OS スレッドにマップされたりする可能性があるためです(例: CUDA)。そのような状況では tokio::task::spawn_blocking を優先してください。

  • 同期ミューテックスは注意して使用してください。.await をまたいでミューテックスを保持すると、別のタスクがブロックされる可能性があり、そのタスクは同じスレッド上で実行されているかもしれません。