๐ŸŽ“ A1Learner
๐Ÿ”

Rust Tokio Blocking

Blocking work is code that holds a thread until it finishes, like a heavy calculation or a normal file read. Blocking work on a runtime thread makes every other task on it wait.

The blocking way: work on the worker thread

This loop runs inside an ordinary task. While it counts, the worker thread can do nothing else โ€” every task scheduled on it waits:

let mut sum: u64 = 0;
for n in 1..=1_000_000u64 { sum += n % 7; }
println!("sum = {sum}");

The same work, moved to the blocking pool

spawn_blocking runs the closure on a spare thread from the blocking pool instead. It returns a JoinHandle, just like spawn โ€” await it the same way:

let heavy = tokio::task::spawn_blocking(|| {
    let mut sum: u64 = 0;
    for n in 1..=1_000_000u64 { sum += n % 7; }
    sum
});
println!("blocking result = {}", heavy.await.unwrap());

A blocking file read moves the same way

std::fs is blocking too. Wrap the call โ€” or use tokio::fs, its async twin:

// blocking std::fs, wrapped so it runs on the blocking pool:
let contents = tokio::task::spawn_blocking(|| std::fs::read_to_string("notes.txt")).await;

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let light = tokio::spawn(async { "quick task done" });

    let heavy = tokio::task::spawn_blocking(|| {
        let mut sum: u64 = 0;
        for n in 1..=1_000_000u64 { sum += n % 7; }
        sum
    });

    println!("{}", light.await.unwrap());
    println!("blocking result = {}", heavy.await.unwrap());
}

Key points

  • Never block a runtime thread: no long calculations, no thread::sleep, no blocking file or network calls
  • The blocking pool is large, with 512 threads by default.
  • But it is not endless.
  • It is for waiting-style work, not thousands of CPU jobs.
  • For heavy pure-CPU work in a long-running program, a crate like rayon is often the better tool
  • tokio::time::sleep pauses a task, not a thread.
  • Blocking work belongs on the blocking pool, not on a runtime thread.

In this example, the number-crunching loop runs inside spawn_blocking, on a thread from the blocking pool. The light task still runs on a normal worker thread and finishes on its own schedule. Awaiting the blocking handle gives 2999998. If the loop had run inside a normal task, it would have hogged a worker thread for the whole count.

Note

std::thread::sleep pauses the whole thread โ€” a classic async bug.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

A task must read a file with std::fs, the blocking API. What should you do?