448 lines
17 KiB
Rust
448 lines
17 KiB
Rust
//! 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<T>`] is
|
|
//! that escape hatch. It behaves like `std::sync::Mutex<T>`, 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<T>` 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<Pid>,
|
|
waiters: VecDeque<Wait>,
|
|
default_timeout: Duration,
|
|
}
|
|
|
|
struct MutexCore {
|
|
state: StdMutex<MutexState>,
|
|
}
|
|
|
|
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<T> {
|
|
core: Arc<MutexCore>,
|
|
/// Protected value. `None` while a guard is live; `Some` while free.
|
|
value: Arc<StdMutex<Option<T>>>,
|
|
}
|
|
|
|
impl<T> Mutex<T> {
|
|
/// 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<MutexGuard<'_, T>, 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<MutexGuard<'_, T>, 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<dyn TimerTarget> = 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<MutexGuard<'_, T>> {
|
|
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<MutexGuard<'_, T>, 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<T> Clone for Mutex<T> {
|
|
/// 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<std::sync::Mutex<...>>.
|
|
unsafe impl<T: Send> Send for Mutex<T> {}
|
|
unsafe impl<T: Send> Sync for Mutex<T> {}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<T>,
|
|
value: Option<T>,
|
|
}
|
|
|
|
impl<T> 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<T> 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<T: std::fmt::Debug> 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<T> 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);
|
|
}
|
|
}
|
|
}
|