Tokio runtime: work stealing and task scheduling
- Last updated
- Prerequisites:
- Rust async/await basics
- Familiarity with Tokio's multi-threaded runtime
- tokio::main macro usage
- rust
- tokio
- async
- runtime
- work-stealing
- scheduler
Read at your depth
The practical view
#[tokio::main] async fn main() starts a Tokio runtime. The multi-threaded runtime (default with the rt-multi-thread feature) spawns one worker thread per core, each with its own task queue, and work-stealing balances load. spawn(async { ... }) schedules a task; await points yield control back to the runtime; timers (tokio::time) and I/O (tokio::net, tokio::fs) integrate with the reactor. The single-threaded runtime (flavor = "current_thread") is the lightweight option for CPU-bound or simple I/O programs. Tasks are Send because the runtime may move them across worker threads — that is why spawned futures must be Send + 'static.
Legacy vs modern
Blocking a worker thread vs proper async spawn
Synchronous blocking inside async code stalls the whole worker; delegating the blocking call to spawn_blocking keeps the scheduler responsive.
async fn handle() {
std::thread::sleep(Duration::from_millis(500)); // blocks the worker
process(&data);
}let data = tokio::task::spawn_blocking(|| fs::read("big.bin")).await??;Interview gotchas
Context
This is the first real-world Tokio pitfall — the interviewer wants the scheduler mechanics (worker starvation), not just 'don't block'.
The mechanical answer
A blocking call (std::fs::read, std::net::TcpStream read, thread::sleep, a lock held across an await) occupies the worker thread for its duration. Tokio cannot preempt the task — it is cooperative — so all other tasks scheduled on that worker stop making progress; with the default worker-per-core setup, N simultaneous blocking calls can stall the entire runtime (the reactor thread still processes events, but polled tasks starve). Tokio detects long-blocking threads (after a 100ms-ish threshold it can grow or emit a warning via the blocking pool heuristic) but it cannot fix it. The correct pattern is tokio::fs/tokio::net or spawn_blocking for CPU/blocking work.
Trap
The trap answer is 'the task will be moved to another thread'. Tasks are not migrated while running — they are only migrated between polls, and a blocked task cannot be polled. Another trap: 'std sleeps are fine because they're short' — any blocking in a hot task skews latency for co-located tasks. Candidates who name the cooperative-polling model and spawn_blocking show they understand why this is a scheduling property, not a style preference.
Context
The interviewer wants the queueing model (local queues, LIFO/FIFO, stealing) and the async-specific semantics (wakeups, cancellation, Send bounds).
The mechanical answer
Each worker has a local run queue. It pops its own tasks LIFO (the most recently spawned is most likely to be cache-hot); when empty, it steals from another worker's queue FIFO (the least recently added, minimizing contention with the owner). If all queues are empty, the worker parks until a waker enqueues something. Unlike a thread pool, tasks are state machines resumed by wakers — a blocked-on-I/O task is not occupying any thread at all, and dropping its JoinHandle simply stops polling it. The Send bound on spawned tasks exists because stealing can move a task to another worker's thread, so the future's captured state must be safe to move across threads.
Trap
The naive answer is 'Tokio spawns a thread per task'. It is M:N — many tasks per worker thread. Another trap: 'cancellation preempts the task' — dropping a task does not interrupt currently-running code; it stops future polls, so a cancelled task that is mid-await just stops being driven. Candidates who describe LIFO/FIFO stealing asymmetry and the waker/reactor handoff demonstrate the real architecture.