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.