You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

关于Tokio的技术疑问:为何CPU密集型任务不应在I/O Executor运行,以及spawn_blocking的优势

Tokio技术疑问:为何CPU密集型任务不应在I/O Executor运行,以及spawn_blocking的优势

Great question! Let's unpack this clearly, using your test code as a reference to highlight the key differences between tokio::spawn and spawn_blocking—even when their runtime looks identical in your specific test case.


First: Why your test shows identical runtimes (and why it's a special case)

In your code, you're running exactly 8 blocking tasks, with a Tokio runtime configured to 8 worker threads. Here's why both approaches take ~1 second:

  • When using tokio::spawn, each blocking task locks up one worker thread entirely. With 8 tasks and 8 workers, all run in parallel, so total time matches the single task duration (1 second).
  • When using spawn_blocking, Tokio routes these tasks to a dedicated blocking thread pool. For 8 concurrent tasks, Tokio will spin up 8 threads in this pool (it dynamically scales based on load), so they also run in parallel, taking 1 second.

This identical runtime hides the critical architectural differences that matter in real-world applications.


The Core Problem with tokio::spawn + Blocking Code

Tokio's worker threads are designed for asynchronous, non-blocking work. Their superpower is handling thousands of lightweight async tasks via cooperative scheduling—switching between tasks only when they await (e.g., waiting for an HTTP response or database query).

When you run blocking code (like std::thread::sleep, or a CPU-heavy loop) in tokio::spawn:

  1. Worker threads get stuck: The blocking code doesn't yield control back to the Tokio scheduler. That worker thread can't process any other async tasks until the blocking code finishes.
  2. Risk of thread starvation: If your app has other async work (e.g., an API server handling incoming requests), those tasks will be delayed or starved if all worker threads are tied up by blocking code.
  3. Wasted resources: Worker threads are a limited, precious resource optimized for async work. Using them for blocking tasks is like using a sports car to haul bricks—you're not using the tool for its intended purpose.

Why spawn_blocking Is the Right Tool for Blocking/CPU-Heavy Work

spawn_blocking was built explicitly for this scenario. Here's how it solves the problems above:

1. Isolates blocking work from async worker threads

Tasks spawned with spawn_blocking run in a separate blocking thread pool, not on Tokio's main worker threads. Your async tasks can continue to run unimpeded on the worker threads while the blocking work happens in parallel.

2. Dynamic, managed thread pool

Tokio automatically scales the blocking thread pool to handle load (default max is 512 threads, configurable) and cleans up idle threads to save resources. You don't have to manage thread creation/destruction yourself.

3. Prevents runtime degradation

Even if you have hundreds of blocking tasks, they won't starve your async workloads. The worker threads remain free to handle I/O-bound async tasks, keeping your app responsive.

4. Handles thread-local storage correctly

spawn_blocking properly manages thread-local state for blocking tasks, avoiding the bugs that can happen when reusing worker threads (which are shared across many async tasks) for blocking code.


A Real-World Example Where the Difference Matters

Let's modify your test to use 16 tasks instead of 8, with 8 worker threads:

  • tokio::spawn approach: Only 8 tasks can run at once (one per worker thread). The first batch takes 1 second, then the second batch takes another 1 second. Total runtime: ~2 seconds.
  • spawn_blocking approach: Tokio will spin up 16 threads in the blocking pool (within the default limit), so all 16 tasks run in parallel. Total runtime: ~1 second.

Worse, if during this time your app had to handle incoming HTTP requests, the tokio::spawn approach would leave zero worker threads free to process those requests—leading to timeouts and unhappy users. The spawn_blocking approach would handle both the blocking tasks and the HTTP requests seamlessly.


Your Code, Annotated

Here's your code with key notes explaining what's happening:

use tokio::time::Instant;
use std::hint::black_box;

fn blocking_call() {
    // Synchronous blocking call - holds the thread it runs on hostage for 1 second
    std::thread::sleep(std::time::Duration::from_millis(1000));
}

#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
async fn main() {
    let n = 8;

    // ----------------------------------------------
    // 1. tokio::spawn with blocking code (BAD - blocks worker threads!)
    // ----------------------------------------------
    let start = Instant::now();

    // Each spawned task locks up one worker thread for 1 second
    let handles: Vec<_> = (0..n)
        .map(|_| tokio::spawn(async move { blocking_call() }))
        .collect();

    for h in handles {
        h.await.unwrap();
    }

    println!("tokio::spawn + blocking code took {:?}", start.elapsed());

    // ----------------------------------------------
    // 2. spawn_blocking with blocking code (GOOD - uses dedicated pool!)
    // ----------------------------------------------
    let start = Instant::now();

    // Each task runs in Tokio's dedicated blocking thread pool
    // Worker threads are only used to await the result, not run the blocking code
    let handles: Vec<_> = (0..n)
        .map(|_| tokio::task::spawn_blocking(|| blocking_call()))
        .collect();

    for h in handles {
        h.await.unwrap();
    }

    println!("spawn_blocking + blocking code took {:?}", start.elapsed());
}

Final Takeaways

  • Never run blocking code in tokio::spawn: It starves the async runtime and kills throughput.
  • Use spawn_blocking for ALL blocking work: This includes CPU-heavy computations (like big math operations) and synchronous I/O (like reading a file with std::fs::read).
  • The test case equality is a red herring: The real value of spawn_blocking is protecting your async workloads from being disrupted by blocking tasks, which becomes critical as your application scales.

火山引擎 最新活动