Module sync/once
One-time initialization primitives.
Once runs a side-effecting closure exactly once; OnceCell(T) stores the
value that closure produced and hands it back on every later call.
Example
{ Once, OnceCell } :: import "std/sync/once";
init := Once.new();
// Only the first call executes the function
init.call(() => {
println("initialized!");
});
init.call(() => {
println("this will NOT run");
});
cell := OnceCell(i32).new();
a := cell.get_or_init(() => i32(42)); // runs the closure
b := cell.get_or_init(() => i32(99)); // returns 42, closure not run
Stability
unstable — the mechanism is settled (double-checked locking over
Mutex.with_lock, concurrently tested in tests/sync/once.test.yo),
but one NAME is wrong against the Rust vocabulary the rest of this
library follows: OnceCell(T) here is thread-safe, which is Rust's
OnceLock<T>, while Rust's OnceCell<T> is the single-threaded one.
Renaming it — or adding a genuinely single-threaded cell beside it — is
a live question, so the name is not frozen. LazyLock's counterpart is
also still missing (plans/STD_API_STABILIZATION_FINDINGS.md item 10);
that one is additive. Neither type has poisoning: an initializer that
exits early leaves the Once un-done and the next caller retries.
Types
Execute a function exactly once, thread-safely. Uses atomic reference counting for safe cross-thread sharing.
Fields
| Name | Type | Description |
|---|---|---|
_done | atomic_bool | |
_mutex | Mutex(bool) |
impl(Once, ...)
new : (Once) fn() -> OnceCreate an empty cell.
Returns: Once
call : (Once) fn(self : Once, f : Impl : (Fn() -> unit)) -> unitRun f the first time this is called, and never again — Rust's
Once::call_once.
Every later call is a no-op, and a call that arrives WHILE another
thread is running f blocks until that f returns, so f has
finished for every caller that this returns to. The already-done case
costs one Acquire load and takes no lock.
_done is set only after f RETURNS, so an f that exits early
leaves the Once un-done and the next caller runs it again. That is
the one divergence from Rust, which poisons a Once whose closure
panicked and then propagates the panic to every later call_once: Yo
has no poisoned state, and marking a half-finished initialization
"done" would be worse than retrying it.
f cannot throw — a closure cannot capture an Exception — so there
is no "the mutex stayed locked" case to worry about; release is
structural through Mutex.with_lock either way.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Once | |
f | Impl : (Fn() -> unit) |
Returns: unit
is_done : (Once) fn(self : Once) -> boolWhether the closure has already run to completion — Rust's
Once::is_completed.
A lock-free Acquire load, so it is cheap, and it is honest in the
direction that matters: true means the initialization is finished
and its writes are visible to this thread. false can be stale the
instant it is read — another thread may be inside f — so do not use
it to decide whether call will block.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Once |
Returns: bool
A cell initialized at most once, thread-safely — the value-carrying
counterpart of Once (Rust's OnceLock).
Once itself stores nothing but its completion flag, so it cannot hand a
value back; OnceCell(T) adds the storage and reuses Once for the
double-checked-locking protocol. The Release-store that publishes _done
happens after _value is written, and every read goes through an
Acquire-load of _done first, so a thread that observes an initialized
cell also observes the value the initializer wrote.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), where(T <: (Send, Acyclic)), OnceCell(T), Acyclic())
impl(generic(T : Type), where(T <: (Send, Acyclic)), OnceCell(T), ...)
new : (fn() -> Self)Create an empty cell.
Returns: Self
get_or_init : (fn(self : Self, f : Impl(Fn() -> T)) -> T)Return the stored value, initializing it with f if this is the first
call. f runs at most once across all threads; every other caller blocks
until it finishes and then sees its value.
Returns: T
get : (fn(self : Self) -> Option(T))Return the stored value, or None if the cell has not been initialized.
Never runs an initializer.
Returns: Option(T)
is_initialized : (fn(self : Self) -> bool)Whether the cell holds a value.
Returns: bool