Skip to main content

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}