Async Runtimes, The Reactor-Executor Pattern & Work-Stealing
Master async runtime internals, the OS Reactor (epoll/kqueue), the multi-threaded Task Executor, and work-stealing queue algorithms.
Unlike Go or JavaScript, Rust does not bundle a runtime into its standard library. The standard library provides only the Future and Poll interfaces.
To execute asynchronous tasks, Rust applications rely on external runtimes like Tokio. Understanding how runtimes combine Reactors and Executors is critical for building enterprise-grade network systems.
When an executor worker thread runs out of tasks, it locklessly steals half the tasks from another worker's local queue.
1. The Reactor-Executor Architecture#
An async runtime is divided into two distinct components:
The Division of Labor#
- The Reactor: Interfaces with kernel event notifications (
epollon Linux,kqueueon macOS,IOCPon Windows). It sleeps until sockets become readable or writable, waking associated tasks. - The Executor: Manages a pool of worker threads that continuously poll active futures to completion.
2. Work-Stealing Scheduling Algorithm#
Modern async runtimes use Work-Stealing to distribute load evenly across multi-core processors:
- Global Queue: A shared queue holding newly spawned tasks.
- Local Queues: Each worker thread maintains a private ring-buffer queue (typically capacity 256).
By processing tasks primarily from local thread queues, worker threads minimize cross-core CPU cache-coherency overhead.
3. The Cardinal Sin: Blocking in Async Code#
Async worker threads rely on tasks yielding cooperatively via .await. If you execute a synchronous, blocking operation (such as reading a file with std::fs, doing heavy image compression, or calling thread::sleep), the entire worker thread is frozen!
12345678910111213141516171819// ANTIPATTERN: Freezes an executor worker thread! async fn bad_handler() { // Blocks the worker thread for 5 seconds! // All other 1,000 tasks assigned to this worker thread are frozen! std::thread::sleep(std::time::Duration::from_secs(5)); } // IDIOMATIC SOLUTION 1: Use non-blocking async primitives async fn good_handler_async() { tokio::time::sleep(std::time::Duration::from_secs(5)).await; } // IDIOMATIC SOLUTION 2: Offload CPU-bound work to blocking pool async fn good_handler_cpu() { let result = tokio::task::spawn_blocking(|| { // Runs on a dedicated thread pool designed for blocking tasks: compute_heavy_cryptography() }).await.unwrap(); }
4. Concept Verification#
Test your understanding of async runtimes and work-stealing schedulers:
How does a work-stealing executor balance computational load across worker threads?
What happens if you execute std::thread::sleep() inside an async function on a Tokio multi-thread runtime?