Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scheduling, Concurrency, Parallelism, and Fairness

One task is polled by at most one worker

The atomic RUNNING bit is a lock around the future or completed output. A task may move between multi-thread workers, but two workers do not poll the same future simultaneously.

Runnable queues are not waiting-task queues

An I/O-waiting task remains owned by the runtime but is absent from run queues. Its waker is registered with a resource. This distinction is why thousands of sleeping tasks do not require repeatedly scanning thousands of queue entries.

Locality first, fairness periodically

Each multi-thread worker has a local queue and one LIFO slot for freshly scheduled local work. The LIFO path improves latency and cache locality in message-passing chains, but Tokio caps consecutive LIFO polls. Workers also check the global injection queue at a tuned interval.

When a local queue is empty, the worker steals from peers. External wake-ups or queue overflow use the synchronized injection path. Idle coordination unparks only enough workers to search for available work.

Cooperative scheduling has a budget

One poll is ordinary Rust code; Tokio cannot preempt it. Runtime-aware async operations consume a cooperative budget. When depleted, they yield so another task can run. A future that performs a long computation without awaiting still blocks its worker.

Parking integrates scheduling with resources

After exhausting and stealing runnable work, a worker parks through the driver. Mio waits for OS readiness with a timeout chosen by the timer wheel. Events wake resource waiters; those wakers schedule tasks back into runtime queues.

Current-thread versus multi-thread

A current-thread runtime uses one executor thread: tasks are concurrent but never poll in parallel. The multi-thread scheduler owns a fixed worker set and can poll different Send tasks simultaneously. LocalSet and the local runtime support !Send futures by keeping them on the owning thread.

Blocking is a separate workload class

spawn_blocking queues a closure to a dynamically managed blocking pool. Its large default thread limit is intended for blocking I/O as well as some CPU work; it is not a CPU-concurrency policy. Large CPU workloads should impose a smaller bound or use a dedicated executor.

The core lesson is that wake-ups create runnable work. Queue policies decide latency and fairness. Worker count decides potential parallelism. None of those by itself limits application-level resources such as database connections or HTTP requests.