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 = ¬ifier_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}