epics_libcom_rs/runtime/sync.rs
1// Re-export tokio sync primitives through the runtime facade.
2pub use std::sync::Arc;
3pub use tokio::sync::{Mutex, Notify, RwLock, broadcast, mpsc, oneshot};
4
5/// Priority-inheritance mutex for real-time builds (epics-base 7-E
6/// `5a8b6e41` "epicsMutex priority inheritance"). On Linux with the
7/// `linux-rt` Cargo feature enabled this wraps a `pthread_mutex_t`
8/// configured with `PTHREAD_PRIO_INHERIT` so a low-priority holder
9/// inherits the priority of the highest-priority waiter — preventing
10/// the classic priority-inversion deadlock that bit C epicsMutex on
11/// PREEMPT_RT kernels.
12///
13/// RTEMS gets the same `pthread_mutex_t` construction on its own arm
14/// below, unconditionally. On every other target — including a default
15/// (non-`linux-rt`) Linux build — this is a transparent type alias for
16/// [`parking_lot::Mutex`], which is
17/// already non-poisoning, smaller, and faster than `std::sync::Mutex`
18/// for typical IOC workloads. Callers see the same API surface — the
19/// PI variant only matters when the OS scheduler can preempt a thread
20/// holding the lock, i.e. when the runtime is configured for
21/// real-time scheduling.
22///
23/// Note: tokio's async `Mutex` is unaffected — async tasks don't have
24/// OS-level priorities to invert. `PriorityInheritanceMutex` is for
25/// the rare blocking-Sync code paths (device-support callbacks, some
26/// tracing sinks) where we hold a mutex while running with
27/// SCHED_FIFO / SCHED_RR.
28#[cfg(all(target_os = "linux", feature = "linux-rt"))]
29pub type PriorityInheritanceMutex<T> = pi_mutex::PiMutex<T>;
30
31/// The RTEMS arm, and it is **not** behind a Cargo feature.
32///
33/// `linux-rt` exists because a desktop Linux build must opt in: the request
34/// can fail on a box without `CAP_SYS_NICE`, and a runaway RT band on a box
35/// that grants it wedges a developer's machine. Neither failure mode exists on
36/// RTEMS, which is the same reasoning that makes
37/// [`crate::runtime::task::DEFAULT_POLICY`] `AllowRealtime` there
38/// (`task.rs:591-603`) — the target has no way to set an env var and no
39/// privilege gate to fail.
40///
41/// The parity target is a POSIX PI mutex, not an RTEMS classic-API semaphore:
42/// base-on-RTEMS-6 compiles the POSIX arm (`configure/toolchain.c:31-35`
43/// selects `OS_API = posix` for `__RTEMS_MAJOR__ >= 5`, and
44/// `os/RTEMS-posix/osdMutex.c:8` is a single `#include "../posix/osdMutex.c"`).
45/// So this is the same construction on the same API as C — including C's
46/// probe-and-fall-back, see `pi_mutex::protocol` below.
47#[cfg(target_os = "rtems")]
48pub type PriorityInheritanceMutex<T> = pi_mutex::PiMutex<T>;
49
50/// Non-RT fallback — uses `parking_lot::Mutex` for the common case.
51/// PI semantics are not needed because the scheduler does not
52/// preempt the lock holder by priority.
53#[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
54pub type PriorityInheritanceMutex<T> = parking_lot::Mutex<T>;
55
56/// The guard `PriorityInheritanceMutex::lock` hands out, nameable so a
57/// caller can *store* one — the per-record write gate
58/// (`server::database::record_lock`) holds a `'static` guard in a struct
59/// field rather than a local.
60///
61/// `!Send` on every arm, and on the PI arms that is a correctness
62/// requirement rather than a lint: POSIX requires a mutex to be unlocked by
63/// the thread that locked it, so a guard that could migrate between threads
64/// would eventually call `pthread_mutex_unlock` from a non-owner. It is also
65/// what makes "no `.await` inside a lock window" a build error at every
66/// spawn site instead of a review convention.
67#[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
68pub type PriorityInheritanceMutexGuard<'a, T> = pi_mutex::PiMutexGuard<'a, T>;
69
70/// See [`PriorityInheritanceMutexGuard`] — `parking_lot`'s guard is already
71/// `!Send` for the same reason.
72#[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
73pub type PriorityInheritanceMutexGuard<'a, T> = parking_lot::MutexGuard<'a, T>;
74
75/// Diagnostic — `true` when the PI variant is active in this build.
76///
77/// On RTEMS this is **not** a `cfg!`: PI there is a probe, exactly as it is in
78/// C (`posix/osdMutex.c:77-85`), so the answer is what the probe obtained
79/// rather than what the build selected. See `pi_mutex::protocol` below.
80pub fn is_pi_mutex_active() -> bool {
81 #[cfg(target_os = "rtems")]
82 {
83 pi_mutex::protocol() == rtems_pi::PTHREAD_PRIO_INHERIT
84 }
85 #[cfg(not(target_os = "rtems"))]
86 {
87 cfg!(all(target_os = "linux", feature = "linux-rt"))
88 }
89}
90
91/// Print, once at boot, which lock protocol this process actually obtained.
92///
93/// C's counterpart is `epicsMutexShowAll`, which reports "PI is/is not
94/// enabled" (`os/posix/osdMutex.c:199-205`) — but that is an iocsh command,
95/// and the RTEMS target has no iocsh. So this is an `eprintln!` from the boot
96/// path, deliberately not a `tracing` event: a subscriber can be absent,
97/// installed late, or filtered, and a diagnostic that reports whether a
98/// guarantee exists must not itself depend on one.
99///
100/// **Two facts on one line, because either alone is misleading.** Priority
101/// inheritance on the record gate needs *both* the probe to have returned
102/// `PTHREAD_PRIO_INHERIT` ([`is_pi_mutex_active`]) *and* the contending
103/// threads to carry distinct scheduling priorities, which is
104/// [`RtPolicy::AllowRealtime`](crate::runtime::task::RtPolicy::AllowRealtime)
105/// — with the RT switch off every thread is one priority and PI has nothing
106/// to inherit (`server::database::record_lock`'s module doc states the same
107/// pair). Printing "PI enabled" beside a disabled RT policy would claim an
108/// ordering the process does not have.
109pub fn report_lock_protocol() {
110 let pi = if is_pi_mutex_active() {
111 "PI is enabled"
112 } else {
113 "PI is not enabled"
114 };
115 eprintln!(
116 "epics-rs: lock protocol: {pi}, RT scheduling {:?}",
117 crate::runtime::task::RtPolicy::current()
118 );
119}
120
121/// The RTEMS priority-protocol surface, declared here rather than taken from
122/// `libc`.
123///
124/// `libc` (the pinned fork, `Cargo.toml`'s `[patch.crates-io]`) declares
125/// `pthread_mutexattr_setprotocol` only for cygwin/qurt/teeos/aix and the
126/// musl/glibc/uclibc modules, and defines `PTHREAD_PRIO_NONE`/`_INHERIT`/
127/// `_PROTECT` only for aix/vxworks/l4re/qurt/apple/hurd/linux — there is no
128/// newlib/rtems arm for either. Both exist on the target: the prototype in
129/// newlib's `pthread.h:189-206`, the constants in `sys/_pthreadtypes.h:81-83`,
130/// and `sys/features.h:394-395` turns `_POSIX_THREAD_PRIO_INHERIT` on
131/// unconditionally for `__rtems__`. So the symbols are there at link time and
132/// only the Rust binding is missing — the identical situation
133/// [`crate::runtime::task`]'s `rtems_sched` block (`task.rs:987-1056`) already
134/// solved for `pthread_setschedparam`.
135///
136/// Deliberately **not** redeclared here: `pthread_mutex_t`,
137/// `pthread_mutexattr_t`, and the `pthread_mutex_*` / `pthread_mutexattr_init`
138/// / `_destroy` functions. `libc` carries all of them for RTEMS at the
139/// target's own widths (`__SIZEOF_PTHREAD_MUTEX_T = 64`,
140/// `__SIZEOF_PTHREAD_MUTEXATTR_T = 24`, `src/unix/newlib/mod.rs:283`, `:329`,
141/// `:392-393`). Restating a struct layout locally is how the `timespec` and
142/// `sockaddr` defects happened; this block declares a function and two
143/// integers and no layout at all.
144#[cfg(target_os = "rtems")]
145mod rtems_pi {
146 use std::ffi::c_int;
147
148 /// `sys/_pthreadtypes.h:81-83` on the arm-rtems6 toolchain.
149 pub const PTHREAD_PRIO_NONE: c_int = 0;
150 /// `sys/_pthreadtypes.h:81-83` on the arm-rtems6 toolchain.
151 pub const PTHREAD_PRIO_INHERIT: c_int = 1;
152
153 unsafe extern "C" {
154 /// `pthread.h:189-206`; implemented in
155 /// `cpukit/posix/src/mutexattrsetprotocol.c`. Absent from `libc`'s
156 /// `newlib/rtems` module.
157 pub fn pthread_mutexattr_setprotocol(
158 attr: *mut libc::pthread_mutexattr_t,
159 protocol: c_int,
160 ) -> c_int;
161 }
162}
163
164/// `pthread_mutex_t` with the target's priority protocol — one implementation
165/// for both PI targets.
166///
167/// Linux and RTEMS differ in exactly one thing — where the protocol constant
168/// comes from (`protocol` below) — so they share the mutex, the guard and the
169/// `unsafe` rather than carrying two copies that must be fixed twice.
170#[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
171mod pi_mutex {
172 use std::cell::UnsafeCell;
173 use std::ffi::c_int;
174 use std::ops::{Deref, DerefMut};
175
176 #[cfg(target_os = "rtems")]
177 use super::rtems_pi;
178 #[cfg(target_os = "rtems")]
179 use super::rtems_pi::pthread_mutexattr_setprotocol;
180 #[cfg(not(target_os = "rtems"))]
181 use libc::pthread_mutexattr_setprotocol;
182
183 /// The protocol every mutex in this process is built with.
184 ///
185 /// On Linux this is `PTHREAD_PRIO_INHERIT` unconditionally: the caller
186 /// asked for `linux-rt`, glibc supports the protocol, and a failure is a
187 /// misconfiguration worth panicking on.
188 #[cfg(not(target_os = "rtems"))]
189 pub fn protocol() -> c_int {
190 libc::PTHREAD_PRIO_INHERIT
191 }
192
193 /// The protocol this process actually obtained, probed once.
194 ///
195 /// C does not assert PI on POSIX — it *probes* it. `globalAttrInit`
196 /// (`posix/osdMutex.c:71-88`) sets `PTHREAD_PRIO_INHERIT` on the global
197 /// attributes, builds one temporary mutex with them, and on failure
198 /// silently downgrades both attributes to `PTHREAD_PRIO_NONE`
199 /// (`:81-85`). `epicsMutexShowAll` then reports which it got
200 /// (`:199-205`).
201 ///
202 /// We match that rather than the Linux arm's `assert_eq!`, and the reason
203 /// is target-specific: the RTEMS IOC installs no `tracing` subscriber and
204 /// has no iocsh, so a panic in a lock constructor during boot is the worst
205 /// available failure mode — it is both fatal and silent. Degrading to a
206 /// plain mutex loses priority inheritance and says so through
207 /// [`is_pi_mutex_active`](super::is_pi_mutex_active); panicking loses the
208 /// IOC.
209 ///
210 /// Probed once per process, so the answer is one fact and not a per-mutex
211 /// race — the `OnceLock` is this file's analogue of C's `pthread_once`.
212 #[cfg(target_os = "rtems")]
213 pub fn protocol() -> c_int {
214 static PROTOCOL: std::sync::OnceLock<c_int> = std::sync::OnceLock::new();
215 *PROTOCOL.get_or_init(|| {
216 // SAFETY: `attr` and `probe` are stack locals of libc's own RTEMS
217 // widths, handed only to the pthread calls that own them, and each
218 // is destroyed on every path that initialised it.
219 unsafe {
220 let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
221 if libc::pthread_mutexattr_init(&mut attr) != 0 {
222 return rtems_pi::PTHREAD_PRIO_NONE;
223 }
224 let mut obtained = rtems_pi::PTHREAD_PRIO_NONE;
225 // C probes only when `setprotocol` itself succeeded
226 // (`osdMutex.c:76`), and treats a failed temporary
227 // `pthread_mutex_init` as "PI does not work here" (`:81`).
228 if pthread_mutexattr_setprotocol(&mut attr, rtems_pi::PTHREAD_PRIO_INHERIT) == 0 {
229 let mut probe: libc::pthread_mutex_t = std::mem::zeroed();
230 if libc::pthread_mutex_init(&mut probe, &attr) == 0 {
231 libc::pthread_mutex_destroy(&mut probe);
232 obtained = rtems_pi::PTHREAD_PRIO_INHERIT;
233 }
234 }
235 libc::pthread_mutexattr_destroy(&mut attr);
236 obtained
237 }
238 })
239 }
240
241 /// The `pthread_mutex_t` lives behind a `Box`, and that is a correctness
242 /// requirement on RTEMS rather than a layout preference.
243 ///
244 /// RTEMS 6 binds a POSIX mutex to **its own address**: `pthread_mutex_init`
245 /// stores `flags = ((uintptr_t) mutex ^ POSIX_MUTEX_MAGIC) | protocol`, and
246 /// every later operation recomputes that from the address it is handed —
247 /// `POSIX_MUTEX_VALIDATE_OBJECT` (`rtems/posix/muteximpl.h:445-459`,
248 /// magic at `:64`) returns **`EINVAL`** when they disagree. So a
249 /// `pthread_mutex_t` that is *relocated* after being initialised is dead:
250 /// not slow, not unordered — every `lock` fails.
251 ///
252 /// Held inline, that is exactly what happened. `new` initialised the mutex
253 /// in a stack local and then moved the struct out by value (and callers
254 /// move it again — `record_lock`'s gates are `Box::leak(Box::new(…))`), so
255 /// on target the first `lock()` returned 22 and the assertion below took
256 /// the IOC down at boot. Measured, `doc/rtems-priority-locks-design.md`
257 /// §5 step 7; invisible on Linux because glibc's mutex carries no address
258 /// in its state and survives the move.
259 ///
260 /// Boxing makes the invariant hold **by construction**: the mutex is
261 /// allocated first and initialised at the address it will keep for its
262 /// whole life, and moving a `PiMutex` moves a pointer. There is no move to
263 /// forbid, so there is no runtime check, no `Pin` in the public API and
264 /// nothing for a future call site to get wrong. It is also what `std` does
265 /// for the same reason on every platform whose `pthread_mutex_t` cannot be
266 /// relocated.
267 ///
268 /// **Not** fixed by zeroing and letting RTEMS auto-initialise: the
269 /// auto-initialisation arm of that macro exists for
270 /// `PTHREAD_MUTEX_INITIALIZER`, and it produces a *default* mutex — no
271 /// protocol, i.e. no priority inheritance. That path would report success
272 /// while silently giving up the property this type exists for.
273 pub struct PiMutex<T> {
274 inner: Box<UnsafeCell<libc::pthread_mutex_t>>,
275 data: UnsafeCell<T>,
276 }
277
278 unsafe impl<T: Send> Send for PiMutex<T> {}
279 unsafe impl<T: Send> Sync for PiMutex<T> {}
280
281 impl<T> PiMutex<T> {
282 pub fn new(value: T) -> Self {
283 // Allocated before it is initialised, so `pthread_mutex_init` sees
284 // the address the mutex keeps for life — see the type's doc.
285 let mutex: Box<UnsafeCell<libc::pthread_mutex_t>> =
286 Box::new(UnsafeCell::new(unsafe { std::mem::zeroed() }));
287 unsafe {
288 let mut attr: libc::pthread_mutexattr_t = std::mem::zeroed();
289 let r = libc::pthread_mutexattr_init(&mut attr);
290 assert_eq!(r, 0, "pthread_mutexattr_init failed");
291 // On RTEMS `protocol()` is the probed value, so this call and
292 // the `pthread_mutex_init` below are being made with exactly
293 // the arguments the probe already proved acceptable. What
294 // remains assertable here is the ENOMEM class, which is C's
295 // `cantProceed` path (`osdMutex.c:98`), not the
296 // PI-unavailable path C degrades on.
297 let protocol = protocol();
298 let r = pthread_mutexattr_setprotocol(&mut attr, protocol);
299 assert_eq!(r, 0, "pthread_mutexattr_setprotocol({protocol}) failed");
300 let r = libc::pthread_mutex_init(mutex.get(), &attr);
301 assert_eq!(r, 0, "pthread_mutex_init failed");
302 libc::pthread_mutexattr_destroy(&mut attr);
303 }
304 Self {
305 inner: mutex,
306 data: UnsafeCell::new(value),
307 }
308 }
309
310 /// The address `pthread_mutex_init` was called with — the one RTEMS
311 /// validates every later operation against.
312 #[cfg(test)]
313 pub fn raw_addr(&self) -> usize {
314 self.inner.get() as usize
315 }
316
317 pub fn lock(&self) -> PiMutexGuard<'_, T> {
318 unsafe {
319 let r = libc::pthread_mutex_lock(self.inner.get());
320 assert_eq!(r, 0, "pthread_mutex_lock failed");
321 }
322 PiMutexGuard {
323 mutex: self,
324 _not_send: std::marker::PhantomData,
325 }
326 }
327 }
328
329 impl<T> Drop for PiMutex<T> {
330 fn drop(&mut self) {
331 unsafe {
332 libc::pthread_mutex_destroy(self.inner.get());
333 }
334 }
335 }
336
337 /// The `parking_lot::Mutex` arm has `Debug`, so this arm must too.
338 ///
339 /// [`PriorityInheritanceMutex`](super::PriorityInheritanceMutex) is one
340 /// type alias with three `cfg` arms, and any trait the fallback arm has
341 /// that a PI arm lacks is a build break that only ever appears on the
342 /// target: a `#[derive(Debug)]` on a struct holding one compiles on a
343 /// developer's box and fails for `armv7-rtems-eabihf`. That is how this
344 /// impl was found — `qsrv::group_config::GroupPvDef` (`#[derive(Debug,
345 /// Clone)]`, holding the L33 atomic-write lock) is the first such struct,
346 /// and it broke nothing until the crate entered the RTEMS gate. The
347 /// guard's `!Send` note above already states the rule for auto traits;
348 /// this is the same rule for the named ones.
349 ///
350 /// `try_lock` rather than `lock`, matching `parking_lot`: a `Debug` impl
351 /// that blocks deadlocks the moment anything formats a structure while the
352 /// lock is held — including a panic message on the locking thread.
353 impl<T: std::fmt::Debug> std::fmt::Debug for PiMutex<T> {
354 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355 // SAFETY: `trylock` never blocks. On success this thread owns the
356 // mutex for the read below and unlocks before returning, so the
357 // POSIX "unlock by the locking thread" rule holds.
358 let acquired = unsafe { libc::pthread_mutex_trylock(self.inner.get()) } == 0;
359 if !acquired {
360 return f
361 .debug_struct("PiMutex")
362 .field("data", &"<locked>")
363 .finish();
364 }
365 let out = f
366 .debug_struct("PiMutex")
367 .field("data", unsafe { &*self.data.get() })
368 .finish();
369 unsafe {
370 libc::pthread_mutex_unlock(self.inner.get());
371 }
372 out
373 }
374 }
375
376 pub struct PiMutexGuard<'a, T> {
377 mutex: &'a PiMutex<T>,
378 /// Makes the guard `!Send`, which `&PiMutex<T>` alone does not.
379 ///
380 /// Not a lint: POSIX requires `pthread_mutex_unlock` to be called by
381 /// the thread that locked the mutex (and an RTEMS PI mutex enforces
382 /// ownership), so a guard that could be moved to another thread and
383 /// dropped there would unlock from a non-owner. `parking_lot`'s
384 /// guard — the arm the host build compiles — is `!Send` for the same
385 /// reason, so this also keeps the two arms' auto traits identical
386 /// and the "no `.await` under a lock" build error target-independent.
387 _not_send: std::marker::PhantomData<*const ()>,
388 }
389
390 impl<T> Deref for PiMutexGuard<'_, T> {
391 type Target = T;
392 fn deref(&self) -> &T {
393 unsafe { &*self.mutex.data.get() }
394 }
395 }
396
397 impl<T> DerefMut for PiMutexGuard<'_, T> {
398 fn deref_mut(&mut self) -> &mut T {
399 unsafe { &mut *self.mutex.data.get() }
400 }
401 }
402
403 impl<T> Drop for PiMutexGuard<'_, T> {
404 fn drop(&mut self) {
405 unsafe {
406 libc::pthread_mutex_unlock(self.mutex.inner.get());
407 }
408 }
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 /// PI mutex API surface — works on both feature gates. Build path:
417 /// confirms the type is constructible and the lock guard derefs.
418 #[test]
419 fn pi_mutex_lock_unlock() {
420 let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(42);
421 {
422 let g = m.lock();
423 assert_eq!(*g, 42);
424 }
425 // re-lock after drop
426 let g = m.lock();
427 assert_eq!(*g, 42);
428 }
429
430 /// The report must follow the arm that was actually compiled, per target.
431 /// Host CI only ever executes the last branch, which is the point: the
432 /// default build must not claim PI it does not have.
433 #[test]
434 fn is_pi_mutex_active_matches_the_cfg_arm() {
435 #[cfg(all(target_os = "linux", feature = "linux-rt"))]
436 assert!(
437 is_pi_mutex_active(),
438 "linux-rt selects the pthread PI arm unconditionally"
439 );
440
441 // On RTEMS the answer is the probe's, not the cfg's — C degrades to
442 // PTHREAD_PRIO_NONE when the target refuses PI
443 // (`posix/osdMutex.c:77-85`) and so do we. What this pins is that the
444 // report and the protocol actually obtained are one fact.
445 #[cfg(target_os = "rtems")]
446 assert_eq!(
447 is_pi_mutex_active(),
448 pi_mutex::protocol() == rtems_pi::PTHREAD_PRIO_INHERIT,
449 "the RTEMS report must be the probe result, not the cfg"
450 );
451
452 #[cfg(not(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems")))]
453 assert!(
454 !is_pi_mutex_active(),
455 "the parking_lot fallback arm has no priority inheritance"
456 );
457 }
458
459 /// The `pthread_mutex_t` must not travel with the `PiMutex` that owns it.
460 ///
461 /// RTEMS binds a POSIX mutex to its own address and returns `EINVAL` from
462 /// every operation on a relocated one (`PiMutex`'s doc). Measured on
463 /// target: with the mutex stored inline the IOC panicked on its first
464 /// `lock()` at boot. No host arm can reproduce *that* — glibc's mutex
465 /// survives the move and the default host build is `parking_lot` — so
466 /// what this pins is the property that makes it impossible: the address
467 /// handed to `pthread_mutex_init` is stable across a move of the owner.
468 /// Storing the mutex inline again fails here rather than on the next boot.
469 #[cfg(any(all(target_os = "linux", feature = "linux-rt"), target_os = "rtems"))]
470 #[test]
471 fn the_pthread_object_does_not_move_with_the_mutex() {
472 let m: PriorityInheritanceMutex<i32> = PriorityInheritanceMutex::new(7);
473 let before = m.raw_addr();
474 let moved = Box::new(m);
475 assert_eq!(
476 moved.raw_addr(),
477 before,
478 "the pthread_mutex_t moved with its owner; RTEMS answers EINVAL to \
479 every lock on a relocated mutex"
480 );
481 assert_eq!(*moved.lock(), 7);
482 }
483
484 /// Whichever arm is compiled must be a mutex. On host CI this is the
485 /// `parking_lot` fallback — the arm the other two tests can only assert
486 /// *about* — so this is where the fallback's exclusion is actually
487 /// exercised.
488 #[test]
489 fn pi_mutex_serialises_concurrent_writers() {
490 const THREADS: u64 = 8;
491 const PER_THREAD: u64 = 10_000;
492
493 let m: Arc<PriorityInheritanceMutex<u64>> = Arc::new(PriorityInheritanceMutex::new(0));
494 let workers: Vec<_> = (0..THREADS)
495 .map(|_| {
496 let m = Arc::clone(&m);
497 std::thread::spawn(move || {
498 for _ in 0..PER_THREAD {
499 *m.lock() += 1;
500 }
501 })
502 })
503 .collect();
504 for w in workers {
505 w.join().expect("worker panicked");
506 }
507 assert_eq!(*m.lock(), THREADS * PER_THREAD);
508 }
509}