Module sync/rwlock
Reader-writer lock allowing multiple concurrent readers or one exclusive writer.
Phase D (THREAD_SAFETY): RwLock(T) is parameterized over the protected
data and access is granted only through a closure, exactly like
Mutex(T).with_lock:
with_read(body)binds the protected value BY VALUE, so for a valueTthe body gets a copy and its writes never reach the shared cell. Yo has no read-only binding mode, so for aref(...)T— where the copy shares the same object — read-only-ness is a convention, not an enforced rule. Do NOT assign to the parameter: that currently degenerates in the evaluator (see issues/assign-to-by-value-closure-param-under-generic-result-types-unit.md).with_write(body)bindsinout(v) : T, so writes land in the protected cell.
The private __RwLockReadUnlocker / __RwLockWriteUnlocker objects
release the lock from their Dispose impl, so the release happens on a
normal return, on an early return, and on an unwind alike.
Example
{ RwLock } :: import "std/sync/rwlock";
lock := RwLock(i32).new(i32(0));
// Many readers may hold the lock at the same time.
value := lock.with_read((v) => v);
// One writer at a time, excluding every reader.
bumped := lock.with_write((v) => {
v = (v + i32(1));
v
});
A body may end in a VALUE or in a STATEMENT — the latter binds the
callback's generic R to unit, which used to emit void* tmp = <void call>
and fail to C-compile (the same hole was in Mutex.with_lock). Fixed
2026-08-26:
issues/fixed/generic-r-callback-with-unit-closure-emits-void-star-temp.md.
Stability
unstable — try_with_read / try_with_write landed 2026-09-10 and the
file has 22 passing cases, but two properties of this surface are still
open questions rather than decisions.
First, READ-ONLY-NESS IS NOT ENFORCED. with_read binds the value by
value because Yo has no read-only binding mode; for a ref(...) T
that copy shares the same object, so a body that mutates through it
mutates shared data while holding only a read lock — and assigning to
the parameter degenerates in the evaluator
(issues/assign-to-by-value-closure-param-under-generic-result-types-unit.md).
Freezing with_read means either a language-level read-only binding or
a different signature.
Second, the lock is READER-PREFERRING and can starve a writer:
_raw_read_lock waits only while a writer HOLDS the lock, not while one
is queued, so an unbroken stream of readers never lets a waiting writer
in. Rust's RwLock leaves the fairness to the OS primitive and says so;
this one implements the policy itself, so the policy is part of the API
and has to be chosen deliberately before it can be frozen.
Types
Reader-writer lock built on Mutex and Cond, owning the protected T.
Uses atomic reference counting for safe cross-thread sharing.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), where(T <: (Send, Acyclic)), RwLock(T), Acyclic())
impl(generic(T : Type), where(T <: (Send, Acyclic)), RwLock(T), ...)
new : (fn(value : T) -> Self)Create a new reader-writer lock protecting value.
Returns: Self
with_read : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(v : T) -> R)
) -> R
)Run body under a SHARED acquisition. Several readers may be inside
with_read at once; no writer can be. body receives the protected
value BY VALUE — for a value T its writes stay in the body's own copy.
Returns: R
with_write : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(inout(v) : T) -> R)
) -> R
)Run body under an EXCLUSIVE acquisition. No other reader or writer can
be inside the lock. body receives inout(v) : T, so its writes land in
the protected cell.
Returns: R
try_with_read : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(v : T) -> R)
) -> Option(R)
)Run body under a SHARED acquisition IF one can be taken without
waiting, and return .Some(result); .None when a writer holds the
lock. Rust's try_read, in this type's closure-scoped shape.
Note this did NOT need the runtime's __yo_mutex_trylock, which the plan
assumed it would. try_read has to test _writer, and that test happens
under the SHORT internal mutex, which is held only for the length of a
few field reads and never across a wait. Blocking on it is bounded and
unrelated to how long a reader or writer holds the RwLock itself — so the
non-blocking part is skipping the condvar wait, not skipping the mutex.
Returns: Option(R)
try_with_write : (
fn(
generic(R : Type),
self : Self,
body : Impl(Fn(inout(v) : T) -> R)
) -> Option(R)
)Run body under an EXCLUSIVE acquisition IF one can be taken without
waiting, and return .Some(result); .None when any reader or a writer
holds the lock. Rust's try_write.
A single concurrent reader is enough to make this .None — an
exclusive acquisition excludes readers too, so this fails far more often
than try_with_read under a read-heavy load. Do not spin on it: a writer
polling against a stream of readers can be starved indefinitely, which is
exactly what the blocking with_write avoids by parking in the queue.
Returns: Option(R)
_raw_read_lock : (fn(self : Self) -> unit)Returns: unit
_raw_try_read_lock : (fn(self : Self) -> bool)Returns: bool
_raw_try_write_lock : (fn(self : Self) -> bool)Returns: bool
_raw_read_unlock : (fn(self : Self) -> unit)Returns: unit
_raw_write_lock : (fn(self : Self) -> unit)Returns: unit
_raw_write_unlock : (fn(self : Self) -> unit)Returns: unit