Skip to main content

tokio/runtime/scheduler/multi_thread/
idle.rs

1//! Coordinates idling workers
2
3use crate::loom::sync::atomic::AtomicUsize;
4use crate::runtime::scheduler::multi_thread::Shared;
5
6use std::fmt;
7use std::sync::atomic::Ordering::{self, SeqCst};
8
9pub(super) struct Idle {
10    /// Tracks both the number of searching workers and the number of unparked
11    /// workers.
12    ///
13    /// Used as a fast-path to avoid acquiring the lock when needed.
14    state: AtomicUsize,
15
16    /// Total number of workers.
17    num_workers: usize,
18}
19
20/// Data synchronized by the scheduler mutex
21pub(super) struct Synced {
22    /// Sleeping workers
23    sleepers: Vec<usize>,
24}
25
26const UNPARK_SHIFT: usize = 16;
27const UNPARK_MASK: usize = !SEARCH_MASK;
28const SEARCH_MASK: usize = (1 << UNPARK_SHIFT) - 1;
29
30#[derive(Copy, Clone)]
31struct State(usize);
32
33impl Idle {
34    pub(super) fn new(num_workers: usize) -> (Idle, Synced) {
35        let init = State::new(num_workers);
36
37        let idle = Idle {
38            state: AtomicUsize::new(init.into()),
39            num_workers,
40        };
41
42        let synced = Synced {
43            sleepers: Vec::with_capacity(num_workers),
44        };
45
46        (idle, synced)
47    }
48
49    /// If there are no workers actively searching, returns the index of a
50    /// worker currently sleeping.
51    pub(super) fn worker_to_notify(&self, shared: &Shared) -> Option<usize> {
52        // If at least one worker is spinning, work being notified will
53        // eventually be found. A searching thread will find **some** work and
54        // notify another worker, eventually leading to our work being found.
55        //
56        // For this to happen, this load must happen before the thread
57        // transitioning `num_searching` to zero. Acquire / Release does not
58        // provide sufficient guarantees, so this load is done with `SeqCst` and
59        // will pair with the `fetch_sub(1)` when transitioning out of
60        // searching.
61        if !self.notify_should_wakeup() {
62            return None;
63        }
64
65        // Acquire the lock
66        let mut lock = shared.synced.lock();
67
68        // Check again, now that the lock is acquired
69        if !self.notify_should_wakeup() {
70            return None;
71        }
72
73        // A worker should be woken up, atomically increment the number of
74        // searching workers as well as the number of unparked workers.
75        State::unpark_one(&self.state, 1);
76
77        // Get the worker to unpark
78        let ret = lock.idle.sleepers.pop();
79        debug_assert!(ret.is_some());
80
81        ret
82    }
83
84    /// Returns `true` if the worker needs to do a final check for submitted
85    /// work.
86    pub(super) fn transition_worker_to_parked(
87        &self,
88        shared: &Shared,
89        worker: usize,
90        is_searching: bool,
91    ) -> bool {
92        // Acquire the lock
93        let mut lock = shared.synced.lock();
94
95        // A worker that is already tracked as a sleeper would be counted twice,
96        // which corrupts `num_unparked` and the sleeper list.
97        debug_assert!(!lock.idle.sleepers.contains(&worker));
98
99        // Decrement the number of unparked threads
100        let ret = State::dec_num_unparked(&self.state, is_searching);
101
102        // Track the sleeping worker
103        lock.idle.sleepers.push(worker);
104
105        ret
106    }
107
108    pub(super) fn transition_worker_to_searching(&self) -> bool {
109        let state = State::load(&self.state, SeqCst);
110        if 2 * state.num_searching() >= self.num_workers {
111            return false;
112        }
113
114        // It is possible for this routine to allow more than 50% of the workers
115        // to search. That is OK. Limiting searchers is only an optimization to
116        // prevent too much contention.
117        State::inc_num_searching(&self.state, SeqCst);
118        true
119    }
120
121    /// A lightweight transition from searching -> running.
122    ///
123    /// Returns `true` if this is the final searching worker. The caller
124    /// **must** notify a new worker.
125    pub(super) fn transition_worker_from_searching(&self) -> bool {
126        State::dec_num_searching(&self.state)
127    }
128
129    /// Unpark a specific worker. This happens if tasks are submitted from
130    /// within the worker's park routine.
131    ///
132    /// Returns `true` if the worker was parked before calling the method.
133    pub(super) fn unpark_worker_by_id(&self, shared: &Shared, worker_id: usize) -> bool {
134        let mut lock = shared.synced.lock();
135        let sleepers = &mut lock.idle.sleepers;
136
137        for index in 0..sleepers.len() {
138            if sleepers[index] == worker_id {
139                sleepers.swap_remove(index);
140
141                // Update the state accordingly while the lock is held.
142                State::unpark_one(&self.state, 0);
143
144                return true;
145            }
146        }
147
148        false
149    }
150
151    /// Returns `true` if `worker_id` is contained in the sleep set.
152    pub(super) fn is_parked(&self, shared: &Shared, worker_id: usize) -> bool {
153        let lock = shared.synced.lock();
154        lock.idle.sleepers.contains(&worker_id)
155    }
156
157    fn notify_should_wakeup(&self) -> bool {
158        // This must be a `SeqCst` RMW rather than a load: it is what makes
159        // the caller's preceding inject-queue push visible to a parking
160        // worker's subsequent unlocked queue-emptiness check.
161        let state = State(self.state.fetch_add(0, SeqCst));
162        state.num_searching() == 0 && state.num_unparked() < self.num_workers
163    }
164}
165
166impl State {
167    fn new(num_workers: usize) -> State {
168        // All workers start in the unparked state
169        let ret = State(num_workers << UNPARK_SHIFT);
170        debug_assert_eq!(num_workers, ret.num_unparked());
171        debug_assert_eq!(0, ret.num_searching());
172        ret
173    }
174
175    fn load(cell: &AtomicUsize, ordering: Ordering) -> State {
176        State(cell.load(ordering))
177    }
178
179    fn unpark_one(cell: &AtomicUsize, num_searching: usize) {
180        cell.fetch_add(num_searching | (1 << UNPARK_SHIFT), SeqCst);
181    }
182
183    fn inc_num_searching(cell: &AtomicUsize, ordering: Ordering) {
184        cell.fetch_add(1, ordering);
185    }
186
187    /// Returns `true` if this is the final searching worker
188    fn dec_num_searching(cell: &AtomicUsize) -> bool {
189        let state = State(cell.fetch_sub(1, SeqCst));
190        state.num_searching() == 1
191    }
192
193    /// Track a sleeping worker
194    ///
195    /// Returns `true` if this is the final searching worker.
196    fn dec_num_unparked(cell: &AtomicUsize, is_searching: bool) -> bool {
197        let mut dec = 1 << UNPARK_SHIFT;
198
199        if is_searching {
200            dec += 1;
201        }
202
203        let prev = State(cell.fetch_sub(dec, SeqCst));
204        debug_assert!(prev.num_unparked() > 0, "{prev:?}");
205        is_searching && prev.num_searching() == 1
206    }
207
208    /// Number of workers currently searching
209    fn num_searching(self) -> usize {
210        self.0 & SEARCH_MASK
211    }
212
213    /// Number of workers currently unparked
214    fn num_unparked(self) -> usize {
215        (self.0 & UNPARK_MASK) >> UNPARK_SHIFT
216    }
217}
218
219impl From<usize> for State {
220    fn from(src: usize) -> State {
221        State(src)
222    }
223}
224
225impl From<State> for usize {
226    fn from(src: State) -> usize {
227        src.0
228    }
229}
230
231impl fmt::Debug for State {
232    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
233        fmt.debug_struct("worker::State")
234            .field("num_unparked", &self.num_unparked())
235            .field("num_searching", &self.num_searching())
236            .finish()
237    }
238}
239
240#[test]
241fn test_state() {
242    assert_eq!(0, UNPARK_MASK & SEARCH_MASK);
243    assert_eq!(0, !(UNPARK_MASK | SEARCH_MASK));
244
245    let state = State::new(10);
246    assert_eq!(10, state.num_unparked());
247    assert_eq!(0, state.num_searching());
248}