Module async/mutex

async/mutex
Stability: unstable — `new`, `lock`, `with_lock`, `try_lock`, `unlock`, `is_locked`, `get` and `set` are intended to keep their names and meanings. What changed under them in 2026-09-11 is the MECHANISM (a waiter queue instead of a 1 ms tick) and one guarantee that is now stronger than it was: acquisition is FIFO, where the timer version documented itself as unfair. Callers that built FIFO ordering on top no longer need to, but nothing they wrote breaks. — stable modules only change additively; this one may still change.

Async (event-loop) mutex — a critical section that may SPAN AWAITS (plans/archive/STD_API_AUDIT.md §7 P0 item 6 / D7). std/sync/mutex's blocking lock inside a future parks the entire single-threaded event loop; this one suspends the waiting task instead.

NOT thread-safe — same-thread tasks only (no Send bound, no atomics, by design: Yo's async runtime is per-thread). It exists because a task that awaits INSIDE a critical section yields the loop mid-section; plain variables are only task-safe between suspension points.

Contended acquisition is WAKER-BASED and FIFO. A task that finds the lock held parks itself and puts its waker at the back of a queue; unlock wakes the one at the front. There is no timer under it, so a hand-off costs a loop turn rather than a millisecond, and the order is the order tasks arrived in. Until 2026-09-11 this re-checked on a 1 ms tick and was explicitly unfair (plans/WAKER_BASED_SCHEDULING.md stage 3).

{ Mutex } :: import "std/async/mutex";
m := Mutex(i64).new(i64(0));
io.await(m.lock(io), io);
// ... awaits allowed here — the section stays exclusive ...
m.set(m.get() + i64(1));
m.unlock();

Stability

unstable — new, lock, with_lock, try_lock, unlock, is_locked, get and set are intended to keep their names and meanings. What changed under them in 2026-09-11 is the MECHANISM (a waiter queue instead of a 1 ms tick) and one guarantee that is now stronger than it was: acquisition is FIFO, where the timer version documented itself as unfair. Callers that built FIFO ordering on top no longer need to, but nothing they wrote breaks.

Types

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

A mutual-exclusion lock for same-thread async tasks, protecting a T.

Type Parameters

NameTypeNotes
TTypecomptime
impl(generic(T : Type), Mutex(T), ...)
new : (fn(value : T) -> Self)

A new unlocked mutex protecting value.

Returns: Self

lock : (fn(self : Self, io : Io) -> Impl(Future(unit)))

Acquire the lock, suspending while another task holds it.

The check and the acquisition happen with no suspension between them, so tasks cannot interleave inside the hand-off. When the lock IS held, the task publishes its waker and then suspends — in that order, which is the order that cannot lose a wake (std/async/waker). It re-checks after waking rather than trusting the wake, because unlock hands over the RIGHT to try, not the lock itself: a try_lock from a third task can still win in between, and the loop would otherwise have two holders.

Returns: Impl(Future(unit))

with_lock : (fn(generic(R : Type), self : Self, body : Impl(Fn(inout(v) : T) -> R), io : Io) -> Impl(Future(R)))

Run body under the lock as one critical section: acquire, run to completion (awaits inside body are allowed — the section stays exclusive across them), release. Was DEFERRED on C54 — a generic -> Impl(Future(R)) method called with two different Rs miscompiled because the async body's body(..) result was stamped as the bare forall R and rendered through the global last-writer registry; the stamp now resolves through the enclosing specialization's env (issues/future-wrapper-return-shared-across-specializations.md, C54).

Returns: Impl(Future(R))

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

Acquire the lock without suspending: true on success.

Returns: bool

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

Release the lock and wake the longest-waiting acquirer, if any. Panics if the mutex is not locked.

The woken task is not handed the lock — it is handed the right to try for it, and it re-checks. That is deliberate: handing over the lock itself would make try_lock from a third task able to observe an unlocked mutex that is already spoken for.

Returns: unit

is_locked : (fn(self : Self) -> bool)

True while some task holds the lock.

Returns: bool

waiter_count : (fn(self : Self) -> usize)

How many tasks are suspended waiting for this lock. Diagnostics only.

Returns: usize

get : (fn(self : Self) -> T)

Read the protected value. Meaningful only while YOU hold the lock — single-threaded cooperation makes an unlocked read a logic bug, not a data race, so it is not prevented.

Returns: T

set : (fn(self : Self, value : T) -> unit)

Replace the protected value (same locking contract as get).

Returns: unit