Module sync/mutex
Mutual exclusion lock primitives.
Phase D (THREAD_SAFETY): Mutex parameterized over the protected data.
Access is granted via with_lock(closure) — the closure receives a
inout(v): T that is second-class (cannot escape the closure scope).
The private __MutexUnlocker handles unlock on both normal return
and unwind via its Dispose impl.
Stability
unstable — the closure-scoped shape (with_lock / try_with_lock, no
guard type) is settled and concurrently tested
(tests/sync/mutex.test.yo, 12/12), but ONE row of
plans/archive/STD_API_STABILIZATION.md §4 "Concurrency" is still open and it
is on this surface: _raw_lock / _raw_unlock / _raw_handle_ptr are
reachable from outside the module and are meant to be removed. They
cannot go yet because Cond.wait takes the raw handle they hand out —
Mutex(T) has no guard to pass instead — so freezing this module waits
on a condvar API that couples to a Mutex(T) without exposing its
primitive. There is also no poisoning: a thread that unwinds out of
body releases the lock and leaves the data as it was, where Rust
would mark the Mutex poisoned. That is a deliberate choice (Yo has no
poisoned state anywhere), not an omission, and it is not what is
holding the freeze.
Types
A mutual-exclusion lock that OWNS the data it protects — Rust's
Mutex<T>, minus the guard.
Access is granted only inside a closure: with_lock hands body an
inout(v) : T that is second-class and cannot escape the closure, so
there is no way to hold a reference to the protected value after the
lock is released. That is the deliberate divergence from Rust, which
returns a MutexGuard and relies on its lifetime; Yo has no lifetimes
to lean on, so the scope is the closure body.
T must be Send (it crosses threads) and Acyclic (the handle is
atomically reference-counted, and atomic RC is only sound for data that
cannot form a cycle). The Acyclic impl below is what lets a
Mutex(T) nest inside another sync container or an std/imm structure.
A Mutex(T) value is a SHARED HANDLE, which is why every method takes
self : Self rather than inout(self): copying the handle (into a
thread closure, say) bumps the atomic reference count and both copies
name the same lock. The OS primitive is destroyed by the Dispose impl
when the last handle goes away, so a mutex cannot outlive its own
destruction — but it also is not destroyed while any thread still holds
a handle.
There is no poisoning: if body unwinds, the lock is released and the
data keeps whatever value it had at that moment.
Misuse traps, on every platform (rule D5 of
plans/reference/PARALLELISM_RULES.md): the lock records the thread that
holds it, so locking it again from that thread, unlocking it from another,
or waiting on a Cond with a mutex the caller does not hold is a panic with
a message naming the API — never the platform-dependent deadlock, silent
recursion or undefined behaviour of the OS primitive underneath.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Acyclic())
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Send())
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Sync())
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), ...)
new : (fn(value : T) -> Self)Wrap value in a new, unlocked mutex — Rust's Mutex::new.
value moves INTO the lock and is thereafter reachable only through
with_lock / try_with_lock. Creating the OS primitive is the only
allocation-time cost; there is no lazy initialization on first lock.
Returns: Self
with_lock : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(inout(v) : T) -> R)
) -> R
)Take the lock, run body on the protected value, release the lock,
and return body's result — Rust's lock().unwrap() plus the guard's
scope, in one call.
This blocks until the lock is available, parking the thread rather
than spinning. body receives an inout(v) : T, so it may mutate the
protected value in place; the mutation is visible to the next holder.
Release is structural, not by falling off the end: a private
__MutexUnlocker is created inside the critical section and its
Dispose impl unlocks, so an early return out of body or an
unwind through this frame still releases the lock. Nothing marks the
mutex poisoned in that case (see the type doc).
Re-entering the same mutex from the same thread PANICS — on every
platform, with a message naming this API. (The POSIX primitive would
deadlock and the Windows CRITICAL_SECTION would silently recurse; the
owner check in _raw_lock replaces both with one trap.) Nest different
mutexes, not the same one.
Returns: R
try_with_lock : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(inout(v) : T) -> R)
) -> Option(R)
)Run body under the lock IF it can be taken without waiting, and
return .Some(result); .None when another thread holds it.
The closure-scoped counterpart of Rust's try_lock. Rust hands back a
Result<MutexGuard, TryLockError>; Mutex(T) deliberately has no guard
type — access is only ever granted inside a closure — so the "did I get
it" answer is the Option instead. body not running IS the failure
case, which is why the result is optional rather than the lock state
being reported separately: there is no way to observe "not acquired" and
still have a value.
Release is by the same __MutexUnlocker with_lock uses, so an early
return or an unwind out of body still unlocks.
Do not spin on this. A while(!took, ...) loop around try_with_lock
is a busy-wait that starves the holder on a single core; with_lock
parks instead. try_with_lock is for the case where there is other
useful work to do when the lock is busy.
The thread that ALREADY holds this mutex gets .None, on every platform
(a Windows CRITICAL_SECTION is recursive and would have said .Some;
the owner check masks that so the answer is portable).
Returns: Option(R)
is_unlocked : (fn(self : Self) -> bool)True when the mutex is NOT held right now.
Advisory only, and racy by construction: it takes the lock to find
out and releases it again, so the answer describes a moment that has
already passed. Never branch on this to decide whether a later
with_lock will block — that is a TOCTOU bug. It exists for assertions
and diagnostics, which is why it is spelled as a question about the past
rather than as can_lock.
Returns: bool
_raw_lock : (fn(self : Self) -> unit)Returns: unit
_raw_unlock : (fn(self : Self) -> unit)Returns: unit
_raw_handle_ptr : (fn(inout(self) : Self) -> *__YO_THREAD_SYNC_TYPE)Returns: *__YO_THREAD_SYNC_TYPE
_held_by_current_thread : (fn(self : Self) -> bool)Whether the calling thread holds this mutex (exact for the caller).
Returns: bool
_park_begin : (fn(self : Self) -> unit)Bracket a condvar wait: the OS wait releases and re-takes the mutex behind our back, so the owner record is cleared before parking and restored after. Both panic-check that the caller holds the mutex.
Returns: unit
_park_end : (fn(self : Self) -> unit)Returns: unit
impl(generic(T : Type), where(T <: (Send, Acyclic)), Mutex(T), Dispose(...))
dispose : (fn(self : Self) -> unit)Release the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the value's
owner drops it (a value type) or the last reference to it goes away (a
reference type), so an implementor never calls it directly and must
tolerate being the only one who ever does.
It must be safe to run exactly once, and a type that also exposes an
explicit close/release is responsible for making the second call a
no-op.
Returns: unit
An UNGUARDED lock with no protected value. lock / try_lock /
unlock are the whole API and balancing them is the caller's job — no
guard releases it for you — so it exists only for the shapes
Mutex.with_lock's closure cannot express: a lock taken in one function
and released in another (the thread pool's submission lock in
std/thread.yo). Prefer Mutex(T) everywhere else; Mutex's own raw
methods are private to std/sync/ (member visibility).
Fields
| Name | Type | Description |
|---|---|---|
_handle | __YO_THREAD_SYNC_TYPE | |
_owner | AtomicUsize |
|
Trait Implementations
impl(RawMutex, Acyclic())
impl(RawMutex, ...)
new : (RawMutex) fn() -> RawMutexWrap value in a new, unlocked mutex — Rust's Mutex::new.
value moves INTO the lock and is thereafter reachable only through
with_lock / try_with_lock. Creating the OS primitive is the only
allocation-time cost; there is no lazy initialization on first lock.
Returns: RawMutex
lock : (RawMutex) fn(self : RawMutex) -> unitBlock until the lock is taken. Locking again from the thread that holds it PANICS (it would deadlock on POSIX and silently recurse on Windows; D5 makes it one behaviour).
Parameters
| Name | Type | Notes |
|---|---|---|
self | RawMutex |
Returns: unit
try_lock : (RawMutex) fn(self : RawMutex) -> boolTake the lock if it is free; true = taken (you must unlock). false
for the thread that already holds it, on every platform (a Windows
CRITICAL_SECTION would re-enter).
Parameters
| Name | Type | Notes |
|---|---|---|
self | RawMutex |
Returns: bool
unlock : (RawMutex) fn(self : RawMutex) -> unitRelease the lock. PANICS unless THIS thread holds it — an unlock without a lock, or from another thread, is undefined behaviour in the OS primitive and is refused here instead.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RawMutex |
Returns: unit
held_by_current_thread : (RawMutex) fn(self : RawMutex) -> boolWhether the calling thread holds this lock. Exact for the caller (only
it can change that fact); the answer about any OTHER thread is
advisory. This is what lets a lock taken in one function be released
in another without a guard — std/thread's submission lock.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RawMutex |
Returns: bool
impl(RawMutex, Dispose(...))
dispose : (RawMutex) fn(self : RawMutex) -> unitRelease the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the value's
owner drops it (a value type) or the last reference to it goes away (a
reference type), so an implementor never calls it directly and must
tolerate being the only one who ever does.
It must be safe to run exactly once, and a type that also exposes an
explicit close/release is responsible for making the second call a
no-op.
Parameters
| Name | Type | Notes |
|---|---|---|
self | RawMutex |
Returns: unit