Module async/mutex
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
A mutual-exclusion lock for same-thread async tasks, protecting a T.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
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