Advanced Concurrency & Asynchrony

the don't-block-the-executor rule

On a one-lane bridge where cars take turns by mutual courtesy, one driver who parks in the middle and goes for lunch freezes everyone behind them. In a cooperatively-scheduled async runtime, the worker thread is that bridge. The don't-block-the-executor rule says: inside an async task, never perform an operation that blocks the OS thread, because while you are blocked you are not yielding, and every other task sharing that worker thread is frozen with you.

Recall that async tasks are cooperatively scheduled: a worker thread advances a task until it hits an await, then moves to the next task. That only works if waiting is done by suspending (yielding the thread), not by blocking it. A blocking call — std::fs::read() (synchronous file I/O), std::thread::sleep(), a synchronous mutex held across work, or a long CPU-bound loop — does not suspend the task; it parks the entire OS thread inside the call. The executor cannot reclaim that thread, so none of the other tasks queued on it make progress. On a single-threaded runtime this can deadlock the whole program; on a multi-threaded one it starves a fraction of capacity and tanks tail latency. The fixes are pointed: use the async equivalent that yields (tokio::fs, tokio::time::sleep, an async mutex), or push genuinely blocking/CPU-heavy work to a dedicated blocking thread pool (spawn_blocking) so it cannot stall the async workers.

This is the single most common async footgun for newcomers, precisely because the blocking call still compiles and often appears to work under light load — then collapses under concurrency when many tasks pile up behind the stalled one. The mental rule to internalise: in async code, 'waiting' must mean 'await', never 'block'. If a function is not async-aware and might wait or churn the CPU for more than a few microseconds, it does not belong directly on an executor thread.

Bad: async fn f() { std::thread::sleep(Duration::from_secs(1)); } // blocks the worker thread, freezing co-located tasks. Good: tokio::time::sleep(Duration::from_secs(1)).await; // yields. Heavy CPU? spawn_blocking(|| crunch()).await.

Synchronous sleep blocks the thread; the async sleep yields it. The difference is invisible until concurrency exposes it.

Blocking code in an async task still compiles and may pass light testing — that is what makes it dangerous. On a single-threaded runtime it can deadlock the entire program; on a multi-threaded one it silently destroys throughput and tail latency.

Also called
async-blocking hazardblocking in async非同步阻塞陷阱