From dd845f22fe6f11d51a4a9aad84b2ae48b9ba155e Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Fri, 24 Jul 2026 08:44:56 +0200 Subject: [PATCH] docs(mutex): user-facing rewrite of the actor-blocking mutex Every public item was previously undocumented. Lead with why Mutex exists (a channel/gen_server is overkill for plain shared state) and how it differs from std::sync::Mutex (parks the actor not the OS thread, every lock is timeout-bounded by default). Add a compiling doctest. Document new/lock/lock_timeout/try_lock/set_default_timeout/ MutexGuard/LockTimeout/DEFAULT_TIMEOUT. Drop em-dashes; keep wake protocol mechanics as contributor-facing comments on private internals. --- src/mutex.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/src/mutex.rs b/src/mutex.rs index ea41872..0fabec3 100644 --- a/src/mutex.rs +++ b/src/mutex.rs @@ -1,12 +1,89 @@ -//! Actor-aware mutex with mandatory timeout. +//! Shared mutable state across actors, when a channel is overkill. //! -//! `Mutex` parks the calling *green* thread on contention rather than -//! blocking the OS thread. Every lock attempt is bounded by a timeout. +//! smarm actors normally coordinate by sending messages, and for a piece of +//! owned state the right tool is usually a `gen_server`: one actor holds the +//! data and everyone else talks to it. Sometimes that is more machinery than +//! you need, and plain shared, lockable state is simpler: [`Mutex`] is +//! that escape hatch. It behaves like `std::sync::Mutex`, guarding a value +//! of type `T` behind a guard that gives you `&mut T` while held, but it is +//! built for smarm's actors rather than OS threads. //! -//! Internals use `Arc>` so the type is genuinely -//! `Send + Sync` and can be shared across scheduler threads. +//! The key difference from `std::sync::Mutex` is what happens on contention. +//! [`Mutex::lock`] parks the calling actor (a cooperatively scheduled green +//! thread) rather than blocking the underlying OS thread, so other actors on +//! the same OS thread keep running while it waits. And every lock attempt is +//! bounded by a timeout: an actor that hangs on to the lock forever (stuck in +//! a bug, or just slow) would otherwise wedge every other actor waiting on +//! it, so smarm makes the wait bounded by default instead of leaving it up +//! to you to remember. //! -//! Fairness: FIFO. Poisoning: none. Reentrance: deadlock (caller bug). +//! ## A first lock +//! +//! ``` +//! use smarm::{run, spawn, Mutex}; +//! +//! run(|| { +//! let counter = Mutex::new(0u32); +//! +//! // Mutex::clone() is cheap and hands out another handle to the SAME +//! // underlying value, much like Arc::clone: every clone shares one lock +//! // and one value, so mutations through one are visible through all. +//! let a = counter.clone(); +//! let b = counter.clone(); +//! +//! let h1 = spawn(move || { +//! let mut guard = a.lock().unwrap(); +//! *guard += 1; +//! }); +//! let h2 = spawn(move || { +//! let mut guard = b.lock().unwrap(); +//! *guard += 1; +//! }); +//! h1.join().unwrap(); +//! h2.join().unwrap(); +//! +//! assert_eq!(*counter.lock().unwrap(), 2); +//! }); +//! ``` +//! +//! ## Choosing a timeout +//! +//! [`Mutex::lock`] waits up to [`DEFAULT_TIMEOUT`] (30 seconds) before giving +//! up with [`LockTimeout`]. To use a different bound for one call, use +//! [`Mutex::lock_timeout`] instead; to change the default for every future +//! `lock()` call on this mutex (including through its clones), use +//! [`Mutex::set_default_timeout`]. If you never want to wait at all, use +//! [`Mutex::try_lock`], which returns immediately whether or not the lock was +//! free. +//! +//! ## Fairness and panics +//! +//! Waiters are granted the lock in the order they started waiting (FIFO), so +//! no actor can be starved by later arrivals repeatedly cutting in line. +//! +//! This mutex never poisons. `std::sync::Mutex` marks itself poisoned if a +//! thread panics while holding the lock, because a partly mutated value might +//! be left behind for the next lock holder to see. smarm's actors already +//! rely on `Drop` running during unwinding to release the lock, so if a +//! holder panics, [`MutexGuard::drop`] still runs and the next waiter is +//! granted the lock normally. It is the same tradeoff `std::sync::Mutex` +//! offers you if you choose to ignore poisoning: you may see a value left +//! mid-update by the panicking actor, so a panic inside a critical section is +//! still a bug worth fixing, just not one that also wedges every future lock +//! attempt. +//! +//! Locking a mutex you already hold (on the same actor) does not queue +//! behind yourself: it deadlocks, the same way relocking a non-reentrant +//! `std::sync::Mutex` does. Don't call `lock` while already holding a guard +//! from the same `Mutex`. +//! +//! ## Outside the runtime +//! +//! `Mutex` also works when called from plain code that is not running as +//! a smarm actor (for example, in a test's setup code before calling +//! [`run`](crate::run)). There, an actor's cooperative park has no meaning, +//! so a lock attempt instead blocks the calling OS thread directly until the +//! mutex is free; there is no timeout on this path. use crate::pid::Pid; use crate::scheduler; @@ -15,8 +92,14 @@ use std::collections::VecDeque; use std::sync::{Arc, Mutex as StdMutex}; use std::time::Duration; +/// How long [`Mutex::lock`] waits for the lock before giving up, unless +/// overridden per-mutex with [`Mutex::set_default_timeout`] or per-call with +/// [`Mutex::lock_timeout`]. pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); +/// Returned by [`Mutex::lock`] / [`Mutex::lock_timeout`] when the timeout +/// elapses before the lock became available. The lock attempt is abandoned; +/// nothing was acquired, and the mutex's value is unaffected. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct LockTimeout; @@ -70,7 +153,7 @@ impl TimerTarget for MutexCore { }; // Remove from waiters only if still there with matching epoch. // If the lock was already granted (holder == Some(pid)), the - // timer fired after the grant — treat as no-op; the actor + // timer fired after the grant: treat as no-op; the actor // will see `is_holder == true` and return Ok. if st.holder == Some(pid) { return; @@ -100,6 +183,8 @@ pub struct Mutex { } impl Mutex { + /// Wrap `value` in a new mutex, initially unlocked, with the default + /// lock timeout ([`DEFAULT_TIMEOUT`]). pub fn new(value: T) -> Self { Self { core: Arc::new(MutexCore::new(DEFAULT_TIMEOUT)), @@ -107,6 +192,11 @@ impl Mutex { } } + /// Change how long future [`lock`](Self::lock) calls on this mutex wait + /// before giving up. Applies to every clone of this `Mutex` (they share + /// one underlying lock), and to `lock` calls already in progress that + /// have not yet started waiting. Does not affect [`lock_timeout`](Self::lock_timeout) + /// calls, which always use the timeout passed in. pub fn set_default_timeout(&self, timeout: Duration) { match self.core.state.lock() { Ok(mut st) => st.default_timeout = timeout, @@ -114,6 +204,12 @@ impl Mutex { } } + /// Acquire the lock, waiting up to this mutex's default timeout + /// ([`DEFAULT_TIMEOUT`], or whatever [`set_default_timeout`](Self::set_default_timeout) + /// last set) if it is currently held elsewhere. Returns a [`MutexGuard`] + /// that releases the lock when dropped, or [`LockTimeout`] if the + /// deadline passes first. To use a one-off timeout instead of the + /// mutex's default, call [`lock_timeout`](Self::lock_timeout) directly. pub fn lock(&self) -> Result, LockTimeout> { let timeout = match self.core.state.lock() { Ok(st) => st.default_timeout, @@ -122,6 +218,10 @@ impl Mutex { self.lock_timeout(timeout) } + /// Acquire the lock, waiting up to `timeout` (ignoring this mutex's + /// default) if it is currently held elsewhere. Returns a [`MutexGuard`] + /// that releases the lock when dropped, or [`LockTimeout`] if `timeout` + /// elapses first with the lock still unavailable. pub fn lock_timeout(&self, timeout: Duration) -> Result, LockTimeout> { // Outside the runtime (e.g. in tests, after run() returns) there is no // current actor PID. Fall back to a blocking std::sync::Mutex acquire. @@ -157,7 +257,7 @@ impl Mutex { Ok(g) => g, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; - // begin_wait is lock-free — legal under the state lock; this + // begin_wait is lock-free (legal under the state lock); this // makes the epoch atomic with the registration's visibility to // grants and timeouts. let epoch = scheduler::begin_wait(); @@ -170,7 +270,7 @@ impl Mutex { scheduler::insert_wait_timer(deadline, me, target, epoch); scheduler::park_current(); - // Resumed — precisely: only our grant or our timer can wake this + // Resumed, precisely: only our grant or our timer can wake this // wait (both epoch-stamped; a stop wake unwinds out of // park_current). The one-shot interpretation below is therefore // exhaustive. Are we the holder? @@ -193,6 +293,9 @@ impl Mutex { } } + /// Acquire the lock only if it is immediately available: never parks and + /// never waits. Returns `Some` with a [`MutexGuard`] if the lock was + /// free, `None` if it is currently held elsewhere. pub fn try_lock(&self) -> Option> { let me = crate::actor::current_pid()?; let mut st = match self.core.state.lock() { @@ -234,6 +337,10 @@ impl Mutex { } impl Clone for Mutex { + /// Cheap: hands back another handle to the same underlying lock and + /// value, the way `Arc::clone` does. All clones of a `Mutex` share one + /// lock and one protected value; locking through any clone excludes + /// every other clone. fn clone(&self) -> Self { Self { core: self.core.clone(), value: self.value.clone() } } @@ -247,6 +354,10 @@ unsafe impl Sync for Mutex {} // Guard // --------------------------------------------------------------------------- +/// Grants access to the value inside a [`Mutex`] while the lock is held. +/// Dereferences to `&T` and `&mut T`. Dropping the guard releases the lock +/// and, if another actor is waiting, wakes the next one in arrival order. +/// Returned by [`Mutex::lock`], [`Mutex::lock_timeout`], and [`Mutex::try_lock`]. pub struct MutexGuard<'a, T> { mutex: &'a Mutex, value: Option,