Module sync/mutex

sync/mutex
Stability: unstable — may still change; see below.

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

__YO_THREAD_SYNC_TYPE

Trait Implementations

Mutex type-function
fn(T : Type) -> Type

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

NameTypeNotes
TTypecomptime

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

RawMutex atomic object
RawMutex

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

NameTypeDescription
_handle__YO_THREAD_SYNC_TYPE
_ownerAtomicUsize

__yo_thread_self() of the holder; 0 = unheld.

Trait Implementations

impl(RawMutex, Acyclic())
impl(RawMutex, ...)
new : (RawMutex) fn() -> RawMutex

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: RawMutex

lock : (RawMutex) fn(self : RawMutex) -> unit

Block 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

NameTypeNotes
selfRawMutex

Returns: unit

try_lock : (RawMutex) fn(self : RawMutex) -> bool

Take 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

NameTypeNotes
selfRawMutex

Returns: bool

unlock : (RawMutex) fn(self : RawMutex) -> unit

Release 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

NameTypeNotes
selfRawMutex

Returns: unit

held_by_current_thread : (RawMutex) fn(self : RawMutex) -> bool

Whether 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

NameTypeNotes
selfRawMutex

Returns: bool

impl(RawMutex, Dispose(...))
dispose : (RawMutex) fn(self : RawMutex) -> 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.

Parameters

NameTypeNotes
selfRawMutex

Returns: unit