Module sync/waitgroup

sync/waitgroup
Stability: unstable — the type has concurrent coverage (`tests/sync/waitgroup.test.yo`) and the Go-derived names are settled, but one behaviour is an unresolved divergence rather than a decision: `add` CLAMPS the counter at zero, so one `done()` too many is silently ignored where Go panics with "negative WaitGroup counter" — the one place Go reliably catches a miscounted group. `plans/STD_API_STABILIZATION_FINDINGS.md` item 10 records it as a gap. Freezing needs that call made (clamp, panic, or a `Result`), because whichever way it goes is observable to every caller. Note also that this is not Go's value type: a `WaitGroup` here is an atomically reference-counted handle, so copying one is correct and shares the counter, where copying Go's is a bug. — stable modules only change additively; this one may still change.

Wait for a group of tasks to complete, similar to Go's sync.WaitGroup.

Example

{ WaitGroup } :: import "std/sync/waitgroup";
{ Thread } :: import "std/thread";

wg := WaitGroup.new();
wg.add(i32(3));
i := i32(0);
while(i < i32(3), {
  t := Thread(unit).spawn((io) => {
    // ... do work ...
    wg.done();
  });
  i = (i + i32(1));
});
wg.wait();  // blocks until all 3 tasks call done()

Stability

unstable — the type has concurrent coverage (tests/sync/waitgroup.test.yo) and the Go-derived names are settled, but one behaviour is an unresolved divergence rather than a decision: add CLAMPS the counter at zero, so one done() too many is silently ignored where Go panics with "negative WaitGroup counter" — the one place Go reliably catches a miscounted group. plans/STD_API_STABILIZATION_FINDINGS.md item 10 records it as a gap. Freezing needs that call made (clamp, panic, or a Result), because whichever way it goes is observable to every caller. Note also that this is not Go's value type: a WaitGroup here is an atomically reference-counted handle, so copying one is correct and shares the counter, where copying Go's is a bug.

Types

WaitGroup atomic object
WaitGroup

Synchronization primitive that blocks until a counter reaches zero. Uses atomic reference counting for safe cross-thread sharing.

The internal counter is an atomic so count() can read without acquiring the mutex (matches Rust's mpsc query-method idiom). Writes to the counter still happen under the mutex so the _count == 0 observation that triggers broadcast() is serialized with wait().

Fields

NameTypeDescription
_countAtomicI32
_mutexMutex(bool)
_cvCond
impl(WaitGroup, ...)
new : (WaitGroup) fn() -> WaitGroup

Create an empty group — counter zero, no waiters. A wait() on it returns immediately until something is added.

Returns: WaitGroup

add : (WaitGroup) fn(self : WaitGroup, delta : i32) -> unit

Add delta to the counter, waking every waiter if it reaches zero — Go's WaitGroup.Add.

Call this BEFORE spawning the tasks it counts: a wait() that runs while the counter is still zero returns immediately, and no later add can bring it back. delta may be negative (done is exactly add(-1)).

The counter is clamped at zero, not checked. A delta that would take it below zero stores zero and broadcasts, so an extra done() is silently absorbed rather than panicking as Go's does — see the module's Stability note.

This briefly takes the group's internal mutex, so it blocks only against another add or the bookkeeping of wait, never for the length of the tasks themselves.

Parameters

NameTypeNotes
selfWaitGroup
deltai32

Returns: unit

done : (WaitGroup) fn(self : WaitGroup) -> unit

Mark one counted task finished — Go's WaitGroup.Done, and exactly add(-1), including the clamp at zero.

Parameters

NameTypeNotes
selfWaitGroup

Returns: unit

wait : (WaitGroup) fn(self : WaitGroup) -> unit

Block until the counter reaches zero — Go's WaitGroup.Wait.

Returns immediately when the counter is already zero, which includes the case of a group nothing was ever added to. Several threads may wait at once; the zero is announced with a broadcast, so all of them wake. The predicate is re-checked in a loop, so a spurious condvar wake cannot end the wait early.

This is a BLOCKING (OS-thread) wait, not an io.await — it parks the calling thread, so calling it on the event-loop thread stalls every async task in the program. Use it from a thread that owns its own work.

Parameters

NameTypeNotes
selfWaitGroup

Returns: unit

count : (WaitGroup) fn(self : WaitGroup) -> i32

How many counted tasks are still outstanding, as a lock-free Acquire load — no mutex is taken.

Advisory only: another thread can change it between the load and the next line, so branching on count() == 0 instead of calling wait() is a race. It exists for diagnostics and progress reporting.

Parameters

NameTypeNotes
selfWaitGroup

Returns: i32