Module sync/cond

sync/cond
Stability: unstable — the operations are Rust's `Condvar` (`wait`, `wait_timeout`, `signal`/`broadcast` for `notify_one`/`notify_all`) and are tested (`tests/sync/cond.test.yo`, 6 cases, added 2026-09-10), but the CALLING CONVENTION is known-wrong and slated to change: `wait` takes a `*__YO_THREAD_SYNC_TYPE` raw pointer that callers can only obtain from `Mutex._raw_handle_ptr()`. `plans/STD_API_STABILIZATION.md` §4 "Concurrency" carries taking `_raw_handle_ptr` off the public surface as an open row, and doing that changes every signature in this file. Freezing follows a `Cond` that couples to a `Mutex(T)` without naming its OS primitive — Rust passes a `MutexGuard`, and `Mutex(T)` deliberately has no guard type to pass. — stable modules only change additively; this one may still change.

Condition variable for thread synchronization.

A Cond parks threads until another thread announces that something they are waiting for may now be true. It carries no state of its own beyond the wait queue: the CONDITION lives in data the caller protects with a Mutex, and every wait must be wrapped in a loop that re-checks it (std/sync/waitgroup.yo's wait is the reference shape).

Stability

unstable — the operations are Rust's Condvar (wait, wait_timeout, signal/broadcast for notify_one/notify_all) and are tested (tests/sync/cond.test.yo, 6 cases, added 2026-09-10), but the CALLING CONVENTION is known-wrong and slated to change: wait takes a *__YO_THREAD_SYNC_TYPE raw pointer that callers can only obtain from Mutex._raw_handle_ptr(). plans/STD_API_STABILIZATION.md §4 "Concurrency" carries taking _raw_handle_ptr off the public surface as an open row, and doing that changes every signature in this file. Freezing follows a Cond that couples to a Mutex(T) without naming its OS primitive — Rust passes a MutexGuard, and Mutex(T) deliberately has no guard type to pass.

Types

Cond atomic object
Cond

Reference-counted condition variable with automatic cleanup via Dispose. Uses atomic reference counting for safe cross-thread sharing.

Fields

NameTypeDescription
cv__YO_COND_TYPE

Trait Implementations

impl(Cond, ...)
new : (Cond) fn() -> Cond

Create a condition variable with an empty wait queue — Rust's Condvar::new.

On Linux the underlying pthread_cond_t is bound to CLOCK_MONOTONIC here, at construction, because the clock is a property of the condvar and not of an individual wait; that is what makes wait_timeout immune to a wall-clock step.

Returns: Cond

wait : (Cond) fn(self : Cond, handle : *(__YO_THREAD_SYNC_TYPE)) -> unit

Block until another thread calls signal or broadcast — Rust's Condvar::wait, with the mutex passed as its raw handle.

handle must be the handle of a mutex this thread currently holds, obtained as m._raw_handle_ptr(). The wait atomically releases that mutex and parks; when it returns, the mutex is held again. Passing the handle of a mutex that is not held, or of a different mutex than other waiters use, is undefined behaviour in the C primitive underneath — this is the sharp edge the open "_raw_handle_ptr off the public surface" row exists to remove.

A return does not mean the condition is true. Condvars are allowed to wake spuriously, and broadcast wakes every waiter including those whose predicate is still false, so the call belongs inside a re-checking loop:

m._raw_lock();
while(runtime(!ready()), {
  cv.wait(m._raw_handle_ptr());
});
m._raw_unlock();

A signal sent while no thread is waiting is LOST — it is not remembered for the next waiter — which is why the predicate must be mutated under the same mutex before signalling. Use wait_timeout when the wait needs a deadline.

Parameters

NameTypeNotes
selfCond
handle*(__YO_THREAD_SYNC_TYPE)

Returns: unit

wait_timeout : (Cond) fn(self : Cond, handle : *(__YO_THREAD_SYNC_TYPE), timeout : Duration) -> bool

Wait until signalled or until timeout elapses. Rust's Condvar::wait_timeout.

Returns true when the wait ended in a wake and false only on a genuine timeout. A true may be a SPURIOUS wake — that is the condvar contract, not a defect — so the caller must re-check its predicate either way:

deadline := Duration.from_millis(i64(500));
while(runtime(!ready()), {
  cond(
    !cv.wait_timeout(m._raw_handle_ptr(), deadline) => {
      // timed out — give up, or re-arm with the remaining budget
      break;
    },
    true => ()
  );
});

The timeout is per WAIT, not per loop. A spurious wake restarts the full timeout, so a loop like the one above can outlast its nominal budget. Track an Instant deadline and pass the REMAINING time if the total matters — the same caveat Rust states, and the reason wait_timeout_while exists there.

The clock is monotonic wherever the platform allows it, so a wall-clock step cannot lengthen or cut short the wait. A negative or zero timeout does not block.

Parameters

NameTypeNotes
selfCond
handle*(__YO_THREAD_SYNC_TYPE)
timeoutDuration

Returns: bool

signal : (Cond) fn(self : Cond) -> unit

Wake ONE waiting thread, if any — Rust's Condvar::notify_one.

Which one is the platform's choice, not FIFO. The call never blocks and does nothing when no thread is waiting; a signal sent to an empty queue is lost, so publish the change to the predicate under the mutex first and signal after. Use broadcast when more than one waiter could be satisfied, or when different waiters are waiting for different predicates on the same condvar — waking the wrong one there loses the notification for good.

Parameters

NameTypeNotes
selfCond

Returns: unit

broadcast : (Cond) fn(self : Cond) -> unit

Wake EVERY waiting thread — Rust's Condvar::notify_all.

Each woken thread re-acquires the mutex one at a time and re-checks its own predicate, so this is the safe default whenever the change could satisfy several waiters (a counter reaching zero, a queue going from empty to non-empty for many consumers). The cost is a thundering herd: N wakes and N re-checks where signal would have done one.

Parameters

NameTypeNotes
selfCond

Returns: unit

impl(Cond, Dispose(...))
dispose : (Cond) fn(self : Cond) -> unit

Release the resources self owns — a file descriptor, a socket, a lock, a buffer the allocator handed out. Called automatically when the last reference to the value goes away, so an implementor never calls it directly and must tolerate being the only one who ever does.

It must be safe to run exactly once: the runtime calls it at refcount zero, and a type that also exposes an explicit close/release is responsible for making the second call a no-op.

Parameters

NameTypeNotes
selfCond

Returns: unit