//! Shared mutable state across actors, when a channel is overkill. //! //! 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. //! //! 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. //! //! ## 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; use crate::timer::{self, TimerTarget}; 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; impl std::fmt::Display for LockTimeout { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "mutex lock timed out") } } impl std::error::Error for LockTimeout {} // --------------------------------------------------------------------------- // Internals // --------------------------------------------------------------------------- struct Wait { pid: Pid, /// The wait's park-epoch (slot-word wait identity, see slot_state.rs). /// Grants and timeouts wake via `unpark_at(pid, epoch)`; a stale entry /// can neither be granted by mistake nor wake the wrong wait. epoch: u32, } struct MutexState { holder: Option, waiters: VecDeque, default_timeout: Duration, } struct MutexCore { state: StdMutex, } impl MutexCore { fn new(default_timeout: Duration) -> Self { Self { state: StdMutex::new(MutexState { holder: None, waiters: VecDeque::new(), default_timeout, }), } } } impl TimerTarget for MutexCore { fn on_timeout(&self, pid: Pid, epoch: u32) { let unpark = { let mut st = match self.state.lock() { Ok(g) => g, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; // 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 // will see `is_holder == true` and return Ok. if st.holder == Some(pid) { return; } match st .waiters .iter() .position(|w| w.pid == pid && w.epoch == epoch) { Some(pos) => { st.waiters.remove(pos); true } None => false, } }; if unpark { scheduler::unpark_at(pid, epoch); } } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- pub struct Mutex { core: Arc, /// Protected value. `None` while a guard is live; `Some` while free. value: Arc>>, } 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)), value: Arc::new(StdMutex::new(Some(value))), } } /// 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, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), } } /// 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, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; 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. let Some(me) = crate::actor::current_pid() else { return self.lock_blocking(); }; // Fast path: nobody holds it. { let mut st = match self.core.state.lock() { Ok(g) => g, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; if st.holder.is_none() { st.holder = Some(me); drop(st); let taken = match self.value.lock() { Ok(mut g) => g.take(), Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), }; let value = match taken { Some(v) => v, None => panic!("smarm: Mutex value missing on free fast path (core corrupt)"), }; return Ok(MutexGuard { mutex: self, value: Some(value), }); } } // Slow path: register as a waiter, set timeout, park. let _np = scheduler::NoPreempt::enter(); let epoch = { let mut st = match self.core.state.lock() { 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 // makes the epoch atomic with the registration's visibility to // grants and timeouts. let epoch = scheduler::begin_wait(); st.waiters.push_back(Wait { pid: me, epoch }); epoch }; let target: Arc = self.core.clone(); let deadline = timer::deadline_from_now(timeout); scheduler::insert_wait_timer(deadline, me, target, epoch); scheduler::park_current(); // 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? let is_holder = match self.core.state.lock() { Ok(st) => st.holder == Some(me), Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; if is_holder { let taken = match self.value.lock() { Ok(mut g) => g.take(), Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), }; let value = match taken { Some(v) => v, None => panic!("smarm: Mutex value missing after grant (core corrupt)"), }; Ok(MutexGuard { mutex: self, value: Some(value), }) } else { Err(LockTimeout) } } /// 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() { Ok(g) => g, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; if st.holder.is_some() { return None; } st.holder = Some(me); drop(st); let taken = match self.value.lock() { Ok(mut g) => g.take(), Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), }; let value = match taken { Some(v) => v, None => panic!("smarm: Mutex value missing on try_lock free path (core corrupt)"), }; Some(MutexGuard { mutex: self, value: Some(value), }) } /// Blocking fallback used when called outside the smarm runtime. /// Spins on the internal std mutex; no actor parking, no timeout. fn lock_blocking(&self) -> Result, LockTimeout> { // We have no PID to register as holder, so we bypass the holder/waiter // tracking and just grab the value mutex directly. This is safe because // outside the runtime there are no green threads competing. let value = loop { let v = match self.value.lock() { Ok(mut g) => g.take(), Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), }; if let Some(v) = v { break v; } std::thread::yield_now(); }; Ok(MutexGuard { mutex: self, value: Some(value), }) } } 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(), } } } // Genuinely Send + Sync now that internals are Arc>. unsafe impl Send for Mutex {} 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, } impl std::ops::Deref for MutexGuard<'_, T> { type Target = T; fn deref(&self) -> &T { match self.value.as_ref() { Some(v) => v, None => panic!("smarm: MutexGuard value missing (core corrupt)"), } } } impl std::ops::DerefMut for MutexGuard<'_, T> { fn deref_mut(&mut self) -> &mut T { match self.value.as_mut() { Some(v) => v, None => panic!("smarm: MutexGuard value missing (core corrupt)"), } } } impl std::fmt::Debug for MutexGuard<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let value = match self.value.as_ref() { Some(v) => v, None => panic!("smarm: MutexGuard value missing (core corrupt)"), }; f.debug_tuple("MutexGuard").field(value).finish() } } impl Drop for MutexGuard<'_, T> { fn drop(&mut self) { let v = match self.value.take() { Some(v) => v, None => panic!("smarm: MutexGuard double drop (core corrupt)"), }; match self.mutex.value.lock() { Ok(mut g) => *g = Some(v), Err(e) => panic!("smarm: mutex value lock poisoned (core corrupt): {e}"), } let next = { let mut st = match self.mutex.core.state.lock() { Ok(g) => g, Err(e) => panic!("smarm: mutex state lock poisoned (core corrupt): {e}"), }; match st.waiters.pop_front() { Some(w) => { st.holder = Some(w.pid); Some((w.pid, w.epoch)) } None => { st.holder = None; None } } }; if let Some((pid, epoch)) = next { scheduler::unpark_at(pid, epoch); } } }