Skip to main content

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}