阻塞执行器

大多数异步运行时支持并发运行 IO 任务。这意味着 CPU 的阻塞性任务会阻塞执行器,并阻止执行其他任务。最简单的方法是,尽可能使用异步等效方法。

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;
}
  • 运行该代码,您会发现休眠操作是连续发生的,而不是并发进行的。

  • "current_thread" 变种将所有任务放在单个线程上。这样做效果会更明显,但 bug 仍然存在于多线程变种中。

  • std::thread::sleep 切换为 tokio::time::sleep,并等待结果。

  • 另一个修复方案是 tokio::task::spawn_blocking,其会生成实际线程并将句柄转换为 Future,且不会阻塞执行器。

  • 不应将任务视为操作系统线程。它们之间并非一对一的映射关系,并且大多数执行器都支持在单个操作系统线程上运行多个任务。尤其是通过 FFI 与其他库交互时,会更容易出现问题,因为在 FFI 中,因为该库可能依赖于线程本地存储或映射到特定的操作系统线程(例如,CUDA)。在这些情况下,首选 tokio::task::spawn_blocking

  • 请谨慎使用同步互斥操作。对 .await 一直执行互斥操作能会导致另一个任务阻塞,并且该任务可能与其在同一线程上运行。