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