mmtk/scheduler/
worker_monitor.rs

1//! This module contains `WorkerMonitor` and related types.  It purposes includes:
2//!
3//! -   letting workers become active or inactive,
4//! -   letting workers park,
5//! -   letting the last parked worker take action, and
6//! -   letting workers and mutators notify workers when workers are given things to do.
7
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::{Condvar, Mutex};
10
11use super::{
12    worker::WorkerShouldExit,
13    worker_goals::{WorkerGoal, WorkerGoals},
14};
15
16/// The result type of the `on_last_parked` call-back in `WorkMonitor::park_and_wait`.
17/// It decides how many workers should wake up after `on_last_parked`.
18pub(crate) enum LastParkedResult {
19    /// The last parked worker should wait, too, until more work packets are added.
20    ParkSelf,
21    /// The last parked worker should unpark and find work packet to do.
22    WakeSelf,
23    /// Unpark the last parked worker and up to `n - 1` others, i.e. just enough workers to cover
24    /// `n` available work packets.
25    Wake(usize),
26    /// Wake up all parked GC workers.  For cases where which worker runs matters (designated
27    /// work) or where every worker must observe a new goal (shutdown, fork).
28    WakeAll,
29}
30
31/// A data structure for synchronizing workers with each other and with mutators.
32///
33/// Unlike `GCWorkerShared`, there is only one instance of `WorkerMonitor`.
34///
35/// -   It allows workers to park and unpark.
36/// -   It allows mutators to notify workers to schedule a GC.
37pub(crate) struct WorkerMonitor {
38    /// The total number of workers.
39    worker_count: usize,
40    /// The synchronized part.
41    sync: Mutex<WorkerMonitorSync>,
42    /// Counts parked workers.  Mutated only while `sync` is held, but kept outside it so that
43    /// `notify_work_available` can read it without acquiring the lock.
44    parker: WorkerParker,
45    /// The number of workers that are allowed to execute work after being notified.
46    active_workers: AtomicUsize,
47    /// Active workers wait on this when idle.  A parked *active* worker is notified if workers
48    /// have things to do.  That includes:
49    /// -   any work packets available, and
50    /// -   any field in `sync.goals.requests` set to true.
51    workers_have_anything_to_do: Condvar,
52    /// Inactive workers wait on this instead.  They are notified whenever the number of active
53    /// workers changes, which may turn them into active workers.
54    active_worker_number_changed: Condvar,
55}
56
57/// The synchronized part of `WorkerMonitor`.
58struct WorkerMonitorSync {
59    /// Current and requested goals.
60    goals: WorkerGoals,
61}
62
63/// This struct counts the number of workers parked and identifies the last parked worker.
64struct WorkerParker {
65    /// The total number of workers.
66    worker_count: usize,
67    /// Number of parked workers.
68    /// The counter can be read without a lock, but can only be mutated while holding the monitor's lock.
69    parked_workers: AtomicUsize,
70}
71
72impl WorkerParker {
73    fn new(worker_count: usize) -> Self {
74        Self {
75            worker_count,
76            parked_workers: AtomicUsize::new(0),
77        }
78    }
79
80    /// Number of workers currently inside `park_and_wait`.
81    /// Deliberately readable without the monitor's lock.
82    fn parked_workers(&self) -> usize {
83        self.parked_workers.load(Ordering::SeqCst)
84    }
85
86    /// Increase the packed-workers counter.
87    /// Called before a worker is parked.  The caller must hold the monitor's lock.
88    ///
89    /// Return true if all the workers are parked.
90    fn inc_parked_workers(&self, _sync: &WorkerMonitorSync) -> bool {
91        let old = self.parked_workers.fetch_add(1, Ordering::SeqCst);
92        debug_assert!(old < self.worker_count);
93        old + 1 == self.worker_count
94    }
95
96    /// Decrease the packed-workers counter.
97    /// Called after a worker is resumed from the parked state.  The caller must hold the
98    /// monitor's lock.
99    fn dec_parked_workers(&self, _sync: &WorkerMonitorSync) {
100        let old = self.parked_workers.fetch_sub(1, Ordering::SeqCst);
101        debug_assert!(old <= self.worker_count);
102        debug_assert!(old > 0);
103    }
104}
105
106impl WorkerMonitor {
107    pub fn new(worker_count: usize) -> Self {
108        Self {
109            worker_count,
110            sync: Mutex::new(WorkerMonitorSync {
111                goals: Default::default(),
112            }),
113            parker: WorkerParker::new(worker_count),
114            active_workers: AtomicUsize::new(worker_count),
115            workers_have_anything_to_do: Default::default(),
116            active_worker_number_changed: Default::default(),
117        }
118    }
119
120    pub(crate) fn active_workers(&self) -> usize {
121        self.active_workers.load(Ordering::SeqCst)
122    }
123
124    pub(crate) fn is_worker_active(&self, ordinal: usize) -> bool {
125        ordinal < self.active_workers()
126    }
127
128    /// Set the number of workers that are allowed to execute work after being notified.
129    ///
130    /// This only updates the count.  Whenever this changes, the caller must also call
131    /// `notify_work_available(true)` (directly, or indirectly via the `on_last_parked`
132    /// call-back while still holding the monitor's internal lock) so that both active and
133    /// inactive workers wake up and re-evaluate whether they should be active.  We don't do the
134    /// notification here because this function may be called while the current thread is the
135    /// last parked worker executing `on_last_parked`, in which case the notification has to
136    /// happen through the already-held lock instead of acquiring it again.
137    pub fn set_active_workers(&self, active_workers: usize) {
138        let active_workers = active_workers.clamp(1, self.worker_count);
139        self.active_workers.store(active_workers, Ordering::SeqCst);
140        debug!(
141            "WorkerMonitor active worker count set to {} (worker_count={}).",
142            active_workers, self.worker_count
143        );
144    }
145
146    /// Make a request.  Can be called by a mutator to request the workers to work towards the
147    /// given `goal`.
148    pub fn make_request(&self, goal: WorkerGoal) {
149        // A request always needs all workers, including currently inactive ones, to make
150        // progress.
151        self.set_active_workers(self.worker_count);
152        let mut guard = self.sync.lock().unwrap();
153        let newly_requested = guard.goals.set_request(goal);
154        if newly_requested {
155            self.notify_work_available_while_locked(true);
156        }
157    }
158
159    /// Wake up workers when more work packets are made available for workers,
160    /// or a mutator has requested the GC workers to schedule a GC.
161    pub fn notify_work_available(&self, all: bool) {
162        // Fast path: no worker is parked, so there is nothing to notify and no reason to
163        // serialise on the monitor lock.  Every `WorkBucket::add` during a GC comes through here,
164        // so this is the difference between one load and a global lock acquisition per packet.
165        if self.parker.parked_workers() == 0 {
166            return;
167        }
168        // We must hold the lock while notifying.  Otherwise a worker that is between checking
169        // its wake-up condition and actually blocking on the CondVar (both of which happen while
170        // holding this lock) could have the notification delivered too early and lost, causing
171        // it to block forever despite the condition it was waiting for having become true.
172        let _guard = self.sync.lock().unwrap();
173        self.notify_work_available_while_locked(all);
174    }
175
176    /// Like `notify_work_available`, but for use by callers that already hold the monitor's
177    /// internal lock (such as `park_and_wait` below), to avoid trying to lock it again.
178    fn notify_work_available_while_locked(&self, all: bool) {
179        if all {
180            self.workers_have_anything_to_do.notify_all();
181            self.active_worker_number_changed.notify_all();
182        } else {
183            self.workers_have_anything_to_do.notify_one();
184        }
185    }
186
187    /// Wake `n` workers in total, counting the caller.  See [`LastParkedResult::Wake`].
188    ///
189    /// Called by the last parked worker while holding `sync`, at which point every other worker is
190    /// provably blocked on `workers_have_anything_to_do`: parking requires `sync`, so each of them
191    /// has already released it inside `Condvar::wait`.  `notify_one` therefore reliably wakes one
192    /// real waiter per call rather than being dropped.
193    fn wake_n_while_locked(&self, n: usize) {
194        // The caller does not wait, so it covers one of the `n` packets itself.
195        let others = n.saturating_sub(1).min(self.worker_count.saturating_sub(1));
196        if others >= self.worker_count.saturating_sub(1) {
197            // Waking everyone anyway; one broadcast beats N wake-ups.
198            self.workers_have_anything_to_do.notify_all();
199        } else {
200            for _ in 0..others {
201                self.workers_have_anything_to_do.notify_one();
202            }
203        }
204    }
205
206    /// Park a worker and wait on the CondVar `workers_have_anything_to_do`.
207    ///
208    /// If it is the last worker parked, `on_last_parked` will be called.
209    /// The argument of `on_last_parked` is true if `sync.gc_requested` is `true`.
210    /// The return value of `on_last_parked` will determine whether this worker and other workers
211    /// will wake up or block waiting.
212    ///
213    /// This function returns `Ok(())` if the current worker should continue working,
214    /// or `Err(WorkerShouldExit)` if the current worker should exit now.
215    pub fn park_and_wait<F>(
216        &self,
217        ordinal: usize,
218        on_last_parked: F,
219    ) -> Result<(), WorkerShouldExit>
220    where
221        F: FnOnce(&mut WorkerGoals) -> LastParkedResult,
222    {
223        let mut sync = self.sync.lock().unwrap();
224
225        // Park this worker
226        let all_parked = self.parker.inc_parked_workers(&sync);
227        trace!(
228            "Worker {} parked.  parked/total: {}/{}.  All parked: {}",
229            ordinal,
230            self.parker.parked_workers(),
231            self.parker.worker_count,
232            all_parked
233        );
234
235        let mut should_wait = false;
236
237        if all_parked {
238            trace!("Worker {} is the last worker parked.", ordinal);
239            let result = on_last_parked(&mut sync.goals);
240            match result {
241                LastParkedResult::ParkSelf => {
242                    should_wait = true;
243                }
244                LastParkedResult::WakeSelf => {
245                    // Continue without waiting.
246                }
247                LastParkedResult::Wake(n) => {
248                    // We are still holding `sync`, so we must not call `notify_work_available`
249                    // (which would try to lock it again and deadlock).
250                    self.wake_n_while_locked(n);
251                }
252                LastParkedResult::WakeAll => {
253                    // We are still holding `sync`, so we must not call `notify_work_available`
254                    // (which would try to lock it again and deadlock).
255                    self.notify_work_available_while_locked(true);
256                }
257            }
258        } else {
259            should_wait = true;
260        }
261
262        // Notes on CondVar usage:
263        //
264        // Conditional variables are usually tested in a loop while holding a mutex
265        //
266        //      lock();
267        //      while condition() {
268        //          condvar.wait();
269        //      }
270        //      unlock();
271        //
272        // The actual condition for this wait is:
273        //
274        // 1.  any work packet is available, or
275        // 2.  a goal (such as doing GC) is requested
276        //
277        // But it is not used like the typical use pattern shown above, mainly because work
278        // packets can be added without holding the mutex `self.sync`.  This means one worker
279        // can add a new work packet (no mutex needed) right after another worker finds no work
280        // packets are available and then park.  In other words, condition (1) can suddenly
281        // become true after a worker sees it is false but before the worker blocks waiting on
282        // the CondVar.  If this happens, the last parked worker will block forever and never
283        // get notified.  This may happen if mutators or the previously existing "coordinator
284        // thread" can add work packets.
285        //
286        // However, after the "coordinator thread" was removed, only GC worker threads can add
287        // work packets during GC.  Parked workers (except the last parked worker) cannot make
288        // more work packets availble (by adding new packets or opening buckets).  For this
289        // reason, the **last** parked worker can be sure that after it finds no packets
290        // available, no other workers can add another work packet (because they all parked).
291        // So the **last** parked worker can open more buckets or declare GC finished.
292        //
293        // Condition (2), i.e. goals added to `sync.goals`, is guarded by the monitor `sync`.
294        // When a mutator adds a goal via `WorkerMonitor::make_request`, it will notify a
295        // worker; and the last parked worker always checks it before waiting.  So this
296        // condition will not be set without any worker noticing.
297        //
298        // Note that generational barriers may add `ProcessModBuf` work packets when not in GC.
299        // This is benign because those work packets are not executed immediately, and are
300        // guaranteed to be executed in the next GC.
301        //
302        // Whether this worker is currently active is a third condition this function waits on.
303        // An inactive worker waits on `active_worker_number_changed` instead of
304        // `workers_have_anything_to_do`, and both are only ever notified while holding
305        // `self.sync` (see `notify_work_available_while_locked`).  Because a worker holds
306        // `self.sync` continuously from the point it decides to wait until the point it
307        // actually blocks inside `Condvar::wait`, a notification can never be delivered to a
308        // worker that has not started waiting yet: the notifier cannot acquire `self.sync`
309        // (and therefore cannot notify) until the worker has released it by starting to wait.
310        //
311        // Notes on spurious wake-up:
312        //
313        // 1.  The condition variable `workers_have_anything_to_do` is guarded by `self.sync`.
314        //     Because the last parked worker is holding the mutex `self.sync` when executing
315        //     `on_last_parked`, no workers can unpark (even if they spuriously wake up) during
316        //     `on_last_parked` because they cannot re-acquire the mutex `self.sync`.
317        //
318        // 2.  Workers may spuriously wake up and unpark when `on_last_parked` is not being
319        //     executed (including the case when the last parked worker is waiting here, too).
320        //     If one or more GC workers spuriously wake up, they will check for work packets,
321        //     and park again if not available.  The last parked worker will ensure the two
322        //     conditions listed above are both false before blocking.  If either condition is
323        //     true, the last parked worker will take action.
324        //
325        // `should_wait` is `false` only when this worker is the last parked worker and
326        // `on_last_parked` returned `WakeSelf` or `WakeAll`, meaning work is already known to be
327        // available and this worker must proceed without waiting here.  Note that `should_wait`
328        // says nothing about whether this worker is currently active: a worker that is not the
329        // last parked one (`should_wait == true`) can still be inactive, e.g. if it was
330        // deactivated by `set_active_workers` before it got here.  Such a worker must not wait on
331        // `workers_have_anything_to_do` (it must not consume work packets while inactive), so it
332        // skips straight to the `while` loop below instead.
333        if should_wait && self.is_worker_active(ordinal) {
334            sync = self.workers_have_anything_to_do.wait(sync).unwrap();
335        }
336
337        // The worker may be inactive already (see above), or it may become inactive while
338        // waiting above, since its active/inactive status can change at any time `self.sync` is
339        // not held.  Keep waiting on `active_worker_number_changed` -- which is notified whenever
340        // the active worker count changes -- until this worker is (re)activated.
341        while !self.is_worker_active(ordinal) {
342            sync = self.active_worker_number_changed.wait(sync).unwrap();
343        }
344
345        // Unpark this worker.
346        self.parker.dec_parked_workers(&sync);
347        trace!(
348            "Worker {} unparked.  parked/total: {}/{}.",
349            ordinal,
350            self.parker.parked_workers(),
351            self.parker.worker_count,
352        );
353
354        // If the current goal is an exit goal, the worker thread should exit.
355        if matches!(
356            sync.goals.current(),
357            Some(WorkerGoal::Shutdown | WorkerGoal::StopForFork)
358        ) {
359            return Err(WorkerShouldExit);
360        }
361
362        Ok(())
363    }
364
365    /// Called when all workers have exited.
366    pub fn on_all_workers_exited(&self) {
367        let mut sync = self.sync.try_lock().unwrap();
368        sync.goals.on_current_goal_completed();
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use std::sync::{
375        atomic::{AtomicBool, AtomicUsize, Ordering},
376        Arc,
377    };
378
379    use super::WorkerMonitor;
380
381    /// Test if the `WorkerMonitor::park_and_wait` method calls the `on_last_parked` callback
382    /// properly.
383    #[test]
384    fn test_last_worker_park_wake_all() {
385        let number_threads = 4;
386        let worker_monitor = Arc::new(WorkerMonitor::new(number_threads));
387        let on_last_parked_called = AtomicUsize::new(0);
388        let should_unpark = AtomicBool::new(false);
389
390        std::thread::scope(|scope| {
391            for ordinal in 0..number_threads {
392                let worker_monitor = worker_monitor.clone();
393                let on_last_parked_called = &on_last_parked_called;
394                let should_unpark = &should_unpark;
395                scope.spawn(move || {
396                    // This emulates the use pattern in the scheduler, i.e. checking the condition
397                    // ("Is there any work packets available") without holding a mutex.
398                    while !should_unpark.load(Ordering::SeqCst) {
399                        println!("Thread {} parking...", ordinal);
400                        worker_monitor
401                            .park_and_wait(ordinal, |_goals| {
402                                println!("Thread {} is the last thread parked.", ordinal);
403                                on_last_parked_called.fetch_add(1, Ordering::SeqCst);
404                                should_unpark.store(true, Ordering::SeqCst);
405                                super::LastParkedResult::WakeAll
406                            })
407                            .unwrap();
408                        println!("Thread {} unparked.", ordinal);
409                    }
410                });
411            }
412        });
413
414        // `on_last_parked` should only be called once.
415        assert_eq!(on_last_parked_called.load(Ordering::SeqCst), 1);
416    }
417
418    /// Like `test_last_worker_park_wake_all`, but only wake up the last parked worker when it
419    /// parked.
420    #[test]
421    fn test_last_worker_park_wake_self() {
422        let number_threads = 4;
423        let worker_monitor = Arc::new(WorkerMonitor::new(number_threads));
424        let on_last_parked_called = AtomicUsize::new(0);
425        let threads_running = AtomicUsize::new(0);
426        let should_unpark = AtomicBool::new(false);
427
428        std::thread::scope(|scope| {
429            for ordinal in 0..number_threads {
430                let worker_monitor = worker_monitor.clone();
431                let on_last_parked_called = &on_last_parked_called;
432                let threads_running = &threads_running;
433                let should_unpark = &should_unpark;
434                scope.spawn(move || {
435                    let mut i_am_the_last_parked_worker = false;
436                    // Record the number of threads entering the following `while` loop.
437                    threads_running.fetch_add(1, Ordering::SeqCst);
438                    while !should_unpark.load(Ordering::SeqCst) {
439                        println!("Thread {} parking...", ordinal);
440                        worker_monitor
441                            .park_and_wait(ordinal, |_goals| {
442                                println!("Thread {} is the last thread parked.", ordinal);
443                                on_last_parked_called.fetch_add(1, Ordering::SeqCst);
444                                should_unpark.store(true, Ordering::SeqCst);
445                                i_am_the_last_parked_worker = true;
446                                super::LastParkedResult::WakeSelf
447                            })
448                            .unwrap();
449                        println!("Thread {} unparked.", ordinal);
450                    }
451                    threads_running.fetch_sub(1, Ordering::SeqCst);
452
453                    if i_am_the_last_parked_worker {
454                        println!("The last parked worker woke up");
455                        // Only the current worker should wake and leave the `while` loop above.
456                        assert_eq!(threads_running.load(Ordering::SeqCst), number_threads - 1);
457                        should_unpark.store(true, Ordering::SeqCst);
458                        worker_monitor.notify_work_available(true);
459                    }
460                });
461            }
462        });
463
464        // `on_last_parked` should only be called once.
465        assert_eq!(on_last_parked_called.load(Ordering::SeqCst), 1);
466    }
467
468    #[test]
469    fn test_only_selected_workers_unpark() {
470        let number_threads = 4;
471        let concurrent_threads = 2;
472        let worker_monitor = Arc::new(WorkerMonitor::new(number_threads));
473        worker_monitor.set_active_workers(concurrent_threads);
474        let first_wave_unparked = AtomicUsize::new(0);
475        let release_everyone = AtomicBool::new(false);
476        let notifier_ran = AtomicBool::new(false);
477
478        std::thread::scope(|scope| {
479            for ordinal in 0..number_threads {
480                let worker_monitor = worker_monitor.clone();
481                let first_wave_unparked = &first_wave_unparked;
482                let release_everyone = &release_everyone;
483                let notifier_ran = &notifier_ran;
484                scope.spawn(move || {
485                    worker_monitor
486                        .park_and_wait(ordinal, |_goals| super::LastParkedResult::WakeAll)
487                        .unwrap();
488
489                    if !release_everyone.load(Ordering::SeqCst) {
490                        first_wave_unparked.fetch_add(1, Ordering::SeqCst);
491                    }
492
493                    if ordinal < concurrent_threads {
494                        while first_wave_unparked.load(Ordering::SeqCst) < concurrent_threads {
495                            std::thread::yield_now();
496                        }
497                        if !notifier_ran.swap(true, Ordering::SeqCst) {
498                            release_everyone.store(true, Ordering::SeqCst);
499                            worker_monitor.set_active_workers(number_threads);
500                            worker_monitor.notify_work_available(true);
501                        }
502                    }
503                });
504            }
505        });
506
507        assert_eq!(
508            first_wave_unparked.load(Ordering::SeqCst),
509            concurrent_threads
510        );
511    }
512}