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