tokio/runtime/blocking/sharded.rs
1//! A sharded queue for the blocking pool's tasks.
2//!
3//! Tasks live in `NUM_SHARDS` queues, each with its own mutex. Spawners push
4//! to a shard chosen via the thread-local RNG; workers pop by scanning the
5//! shards, starting from one derived from their worker id. A mask tracks which
6//! shards have tasks so scans rarely lock empty shards.
7//!
8//! Worker lifecycle (thread spawning, parking/waking, timeouts, shutdown) is
9//! coordinated by the `coord` mutex + `condvar`, using the same claim
10//! protocol as the default single-mutex queue. Unlike that queue, `coord` is
11//! held only for the claim-or-spawn decision and for a worker's transition
12//! to idle — never around queue operations or task execution — so spawners
13//! and workers contend mostly on `1/NUM_SHARDS` of a shard lock each.
14
15use crate::loom::sync::atomic::{AtomicBool, AtomicUsize};
16use crate::loom::sync::{Condvar, Mutex};
17use crate::loom::thread;
18
19use std::collections::VecDeque;
20use std::sync::atomic::Ordering;
21use std::time::Duration;
22
23use super::pool::{ShutdownHandles, SpawnError, SpawnerMetrics, Task, ThreadManagementState};
24
25/// Number of shards. Must be a power of two.
26///
27/// Under loom, use a small shard count to keep the state space tractable
28/// while still exercising the cross-shard scanning in `pop`.
29#[cfg(not(loom))]
30const NUM_SHARDS: usize = 16;
31#[cfg(loom)]
32const NUM_SHARDS: usize = 2;
33
34struct Shard {
35 queue: VecDeque<Task>,
36 /// Set (under the shard's lock) when the shard is drained for shutdown;
37 /// pushes to a sealed shard are rejected. This is what guarantees that a
38 /// spawner racing with shutdown cannot leave a task behind: a push
39 /// either loses (rejected, task is shut down) or wins, in which case the
40 /// sealer that later drains this shard collects the task.
41 sealed: bool,
42}
43
44pub(super) struct ShardedImpl {
45 shards: [Mutex<Shard>; NUM_SHARDS],
46 /// One bit per shard, set when that shard's queue is non-empty. Only
47 /// updated while holding that shard's lock, so it is exact at every
48 /// lock release; unlocked loads may be stale (see `pop`).
49 non_empty_mask: AtomicUsize,
50 coord: Mutex<ShardedCoord>,
51 condvar: Condvar,
52 /// Mirror of `ThreadManagementState::shutdown`, to let spawners reject
53 /// tasks without taking `coord`.
54 is_shutdown: AtomicBool,
55 /// Round-robin push counter, used to pick a shard when the thread-local
56 /// RNG is unavailable (loom requires each execution to be deterministic).
57 #[cfg(loom)]
58 push_index: AtomicUsize,
59}
60
61/// State protected by `ShardedImpl::coord`. This mirrors `LockedInner`,
62/// except that the queue itself lives in the shards.
63struct ShardedCoord {
64 /// Pending worker wakeups. A spawner claims an idle worker by
65 /// decrementing `num_idle_threads` and incrementing this; a woken worker
66 /// acknowledges by decrementing it. Distinguishes real wakeups from
67 /// spurious ones.
68 num_notify: u32,
69 thread_mgmt_state: ThreadManagementState,
70}
71
72impl ShardedImpl {
73 pub(super) fn new(thread_mgmt_state: ThreadManagementState) -> ShardedImpl {
74 ShardedImpl {
75 shards: std::array::from_fn(|_| {
76 Mutex::new(Shard {
77 queue: VecDeque::new(),
78 sealed: false,
79 })
80 }),
81 non_empty_mask: AtomicUsize::new(0),
82 coord: Mutex::new(ShardedCoord {
83 num_notify: 0,
84 thread_mgmt_state,
85 }),
86 condvar: Condvar::new(),
87 is_shutdown: AtomicBool::new(false),
88 #[cfg(loom)]
89 push_index: AtomicUsize::new(0),
90 }
91 }
92
93 /// Pick a shard to push to. Use the thread-local RNG so that concurrent
94 /// spawners spread across the shards.
95 #[cfg(not(loom))]
96 fn push_shard_index(&self) -> usize {
97 crate::runtime::context::thread_rng_n(NUM_SHARDS as u32) as usize
98 }
99
100 /// Under loom the RNG would make each execution take a different path,
101 /// breaking loom's requirement that executions be deterministic, so use
102 /// round-robin selection instead.
103 #[cfg(loom)]
104 fn push_shard_index(&self) -> usize {
105 self.push_index.fetch_add(1, Ordering::Relaxed) % NUM_SHARDS
106 }
107
108 /// Push a task onto one of the shards, or hand it back if the chosen
109 /// shard has been sealed for shutdown.
110 ///
111 /// The queue-depth metric is incremented under the shard lock so that
112 /// the pop that consumes this task (whose decrement is ordered after
113 /// this lock's release) can never transiently wrap the counter.
114 fn push(&self, task: Task, metrics: &SpawnerMetrics) -> Result<(), Task> {
115 let index = self.push_shard_index();
116 let mut shard = self.shards[index].lock();
117 if shard.sealed {
118 return Err(task);
119 }
120 shard.queue.push_back(task);
121 metrics.inc_queue_depth();
122 if shard.queue.len() == 1 {
123 self.non_empty_mask.fetch_or(1 << index, Ordering::Relaxed);
124 }
125 Ok(())
126 }
127
128 /// Pop a task, checking the worker's preferred shard first.
129 fn pop(&self, preferred_shard: usize) -> Option<Task> {
130 let mask = self.non_empty_mask.load(Ordering::Relaxed);
131 if mask == 0 {
132 return None;
133 }
134
135 let start = preferred_shard % NUM_SHARDS;
136 for i in 0..NUM_SHARDS {
137 let index = (start + i) % NUM_SHARDS;
138 if mask & (1 << index) == 0 {
139 continue;
140 }
141
142 let mut shard = self.shards[index].lock();
143 match shard.queue.pop_front() {
144 Some(task) => {
145 if shard.queue.is_empty() {
146 self.non_empty_mask
147 .fetch_and(!(1 << index), Ordering::Relaxed);
148 }
149 return Some(task);
150 }
151 None => {
152 // The shard was emptied (and its bit cleared) after the
153 // mask was loaded; move on to the next candidate.
154 }
155 }
156 }
157
158 None
159 }
160
161 /// Drain every shard, sealing each so that later pushes are rejected,
162 /// and run-or-cancel the collected tasks. Called by workers during
163 /// shutdown (and by `begin_shutdown` when there are no workers left to
164 /// do it). Sealing is idempotent, so concurrent callers are fine.
165 fn drain_and_seal(&self, metrics: &SpawnerMetrics, preferred_shard: usize) {
166 let start = preferred_shard % NUM_SHARDS;
167 for i in 0..NUM_SHARDS {
168 let index = (start + i) % NUM_SHARDS;
169 let tasks = {
170 let mut shard = self.shards[index].lock();
171 shard.sealed = true;
172 self.non_empty_mask
173 .fetch_and(!(1 << index), Ordering::Relaxed);
174 std::mem::take(&mut shard.queue)
175 };
176 for task in tasks {
177 metrics.dec_queue_depth();
178 task.shutdown_or_run_if_mandatory();
179 }
180 }
181 }
182
183 /// Push a task and either notify an idle worker or invoke
184 /// `on_no_idle` (which is responsible for spawning a new worker if
185 /// possible).
186 pub(super) fn spawn_task<F>(
187 &self,
188 task: Task,
189 metrics: &SpawnerMetrics,
190 on_no_idle: F,
191 ) -> Result<(), SpawnError>
192 where
193 F: FnOnce(&mut ThreadManagementState) -> Result<(), SpawnError>,
194 {
195 if self.is_shutdown.load(Ordering::Acquire) {
196 // It's fine to shutdown this task (even if mandatory): it was
197 // scheduled after the shutdown of the runtime began.
198 task.shutdown();
199 return Err(SpawnError::ShuttingDown);
200 }
201
202 // Push before taking `coord`, so spawners don't serialize on a
203 // pool-wide lock held across the queue operation.
204 if let Err(task) = self.push(task, metrics) {
205 // The shard was already drained and sealed for shutdown: reject
206 // the task, exactly as if the shutdown check above had caught it.
207 task.shutdown();
208 return Err(SpawnError::ShuttingDown);
209 }
210
211 let mut coord = self.coord.lock();
212
213 if coord.thread_mgmt_state.shutdown {
214 // Shutdown raced with our push, but the push beat the seal, so
215 // whichever worker (or `begin_shutdown`) seals that shard is
216 // guaranteed to collect the task and run it (if mandatory) or
217 // shut it down. Nothing to do here.
218 return Ok(());
219 }
220
221 if metrics.num_idle_threads() == 0 {
222 on_no_idle(&mut coord.thread_mgmt_state)?;
223 } else {
224 // Claim an idle worker (see `num_notify`). Signal after
225 // releasing `coord` so the woken worker doesn't immediately
226 // block on it; the counter increment, made under `coord`, is
227 // what guarantees the wakeup cannot be lost.
228 metrics.dec_num_idle_threads();
229 coord.num_notify += 1;
230 drop(coord);
231 self.condvar.notify_one();
232 }
233
234 Ok(())
235 }
236
237 /// Run a worker thread's main loop.
238 pub(super) fn run_worker(
239 &self,
240 metrics: &SpawnerMetrics,
241 keep_alive: Duration,
242 worker_thread_id: usize,
243 ) -> Option<thread::JoinHandle<()>> {
244 let mut join_on_thread = None;
245 let mut coord;
246
247 'main: loop {
248 // BUSY: run tasks without holding `coord`, so that spawners and
249 // other workers are not blocked on this worker.
250 while let Some(task) = self.pop(worker_thread_id) {
251 metrics.dec_queue_depth();
252 task.run();
253 }
254
255 coord = self.coord.lock();
256
257 // Re-check the shards under `coord` before going idle: a task
258 // may have been pushed after the scan above, its spawner seeing
259 // this worker as busy and so neither notifying nor spawning.
260 // `coord` orders this re-check against every claim-or-spawn
261 // decision: a spawner that decided first pushed (and set the
262 // mask bit) before its `coord` critical section, so the task is
263 // visible here; one that decides later sees this worker counted
264 // idle and claims it.
265 if let Some(task) = self.pop(worker_thread_id) {
266 metrics.dec_queue_depth();
267 drop(coord);
268 task.run();
269 continue 'main;
270 }
271
272 // IDLE
273 metrics.inc_num_idle_threads();
274
275 while !coord.thread_mgmt_state.shutdown {
276 let lock_result = self.condvar.wait_timeout(coord, keep_alive).unwrap();
277
278 coord = lock_result.0;
279 let timeout_result = lock_result.1;
280
281 if coord.num_notify != 0 {
282 // A legitimate wakeup; the spawner already decremented
283 // `num_idle_threads` on this worker's behalf.
284 coord.num_notify -= 1;
285 drop(coord);
286 continue 'main;
287 }
288
289 // Even if the condvar "timed out", if the pool is
290 // entering the shutdown phase, we want to perform
291 // the cleanup logic.
292 if !coord.thread_mgmt_state.shutdown && timeout_result.timed_out() {
293 join_on_thread = coord.thread_mgmt_state.worker_timed_out(worker_thread_id);
294
295 break 'main;
296 }
297
298 // Spurious wakeup detected, go back to sleep.
299 }
300
301 // The pool is shutting down: drain and seal the shards, so that
302 // a spawner racing with shutdown either gets its task collected
303 // here or gets its push rejected (see `spawn_task`).
304 drop(coord);
305 self.drain_and_seal(metrics, worker_thread_id);
306
307 coord = self.coord.lock();
308 break 'main;
309 }
310
311 // Thread exit
312 metrics.dec_num_threads();
313
314 // Unlike `LockedImpl`, this worker is always counted in
315 // `num_idle_threads` here: both loop exits (timeout and shutdown)
316 // are reached after the IDLE transition without a spawner having
317 // claimed this worker.
318 let prev_idle = metrics.dec_num_idle_threads();
319 assert_ne!(
320 prev_idle, 0,
321 "`num_idle_threads` underflowed on thread exit"
322 );
323
324 if coord.thread_mgmt_state.shutdown && metrics.num_threads() == 0 {
325 self.condvar.notify_one();
326 }
327
328 drop(coord);
329
330 join_on_thread
331 }
332
333 /// Begin pool shutdown: set the shutdown flag, drop the shutdown
334 /// sender, wake all waiting workers, and hand back the worker
335 /// `JoinHandle`s for the caller to join.
336 pub(super) fn begin_shutdown(&self, metrics: &SpawnerMetrics) -> Option<ShutdownHandles> {
337 let mut coord = self.coord.lock();
338 let handles = coord.thread_mgmt_state.begin_shutdown()?;
339 self.is_shutdown.store(true, Ordering::Release);
340 self.condvar.notify_all();
341
342 // Every live worker seals the shards on its way out, but if there
343 // are no workers (and none can be spawned now that `shutdown` is
344 // set), seal here so a racing spawner's push can't be stranded.
345 let no_workers = metrics.num_threads() == 0;
346 drop(coord);
347 if no_workers {
348 self.drain_and_seal(metrics, 0);
349 }
350
351 Some(handles)
352 }
353}