You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The user-mode Futex (src/sync/Futex.zig) currently supports the basic pair:
wait(ptr, expect) — value-checked at registration, parks on the address.
wake(ptr, max) — wakes up to max waiters parked on that exact address.
A wake is unconditional per address: every parked waiter on the address is a candidate, and blocking callers re-check their condition in a loop after waking (e.g. Group.wait()).
PR #551 (select/wait on Group) parks the select waiter directly on the group's state word via a new non-blocking prepareWait()/cancelWait(). That works today only becauseunregisterGroupTask calls wake() solely on the counter→0 transition, so the select waiter is effectively edge-triggered on real completion. select's protocol is one-shot and cannot re-arm, so it relies on "a wake means the condition holds."
That invariant is fragile:
If we ever wake on other events on the same word — e.g. the fail_fast early-wake TODOs in group.zig (wake on failed/canceled while counter > 0) — a select-on-completion waiter would be woken and would report completion prematurely.
More generally, distinct conditions packed into one word (here: counter + flag bits) can't currently have distinct waiters on the same address.
Proposal
Add a bitset-based conditional wake, mirroring Linux FUTEX_WAIT_BITSET / FUTEX_WAKE_BITSET:
Each waiter registers an interest mask.
Each wake carries an event mask.
The wake loop signals a node only when waiter_mask & wake_mask != 0; non-matching nodes stay parked.
This keeps the waker in charge of naming the event (which it always knows cheaply), so no memory value is re-read in the wake loop — just an & of two u32s. It is a strict generalization: the existing behavior is the all-ones mask.
asyncWait registers interest COMPLETE; unregisterGroupTask wakes with COMPLETE on counter→0.
A future fail_fast early-wake: setFailed/setCanceled wake with FAILED/CANCELED. Completion-only waiters are no longer woken by failure events, and vice-versa — the premature-wake caveat from Support select() and wait() on Group #551 dissolves.
Why bitset and not "value re-check at wake"
There's an alternative: have the wake loop re-read the word and re-evaluate a per-node masked-value predicate ((word & mask) == target). That handles "wake blindly / on every finish," but it is not a standard Linux primitive, and it adds an acquire-load plus ordering reasoning inside the wake loop. Since our wakers always know which event they're signaling, the bitset approach is cheaper (no load in the wake loop), matches the real Linux semantics, and composes better. Prefer bitset.
General usefulness
This isn't group-specific. A conditional wake keyed by interest bits is broadly useful for anyone building sync primitives on the futex:
Reader/writer locks (wake only readers, or only a writer).
Event-typed wakeups where several distinct conditions share one word.
Reducing thundering-herd wakeups by targeting the subset that cares.
Scope / non-goals
In scope: WAIT_BITSET / WAKE_BITSET semantics via an interest mask on the waiter + an event mask on a wake variant.
Out of scope (speculative for now): FUTEX_WAKE_OP (atomic modify + compare on a second word) and FUTEX_CMP_REQUEUE (condvar-style requeue). No current use case.
Contract note
Bitset moves a small, explicit obligation onto wakers: if you flip a bit someone can wait on, you must issue a wake carrying that bit. That's easier to get right than a value-re-check's ordering, but worth documenting alongside the API.
Background
The user-mode
Futex(src/sync/Futex.zig) currently supports the basic pair:wait(ptr, expect)— value-checked at registration, parks on the address.wake(ptr, max)— wakes up tomaxwaiters parked on that exact address.A wake is unconditional per address: every parked waiter on the address is a candidate, and blocking callers re-check their condition in a loop after waking (e.g.
Group.wait()).PR #551 (select/wait on
Group) parks the select waiter directly on the group's state word via a new non-blockingprepareWait()/cancelWait(). That works today only becauseunregisterGroupTaskcallswake()solely on the counter→0 transition, so the select waiter is effectively edge-triggered on real completion.select's protocol is one-shot and cannot re-arm, so it relies on "a wake means the condition holds."That invariant is fragile:
fail_fastearly-wakeTODOs ingroup.zig(wake onfailed/canceledwhilecounter > 0) — a select-on-completion waiter would be woken and would report completion prematurely.Proposal
Add a bitset-based conditional wake, mirroring Linux
FUTEX_WAIT_BITSET/FUTEX_WAKE_BITSET:waiter_mask & wake_mask != 0; non-matching nodes stay parked.This keeps the waker in charge of naming the event (which it always knows cheaply), so no memory value is re-read in the wake loop — just an
&of twou32s. It is a strict generalization: the existing behavior is the all-ones mask.Sketch
FutexWaitergainswake_bits: u32 = ~0(all-ones default preserves currentwait()/prepareWait()semantics exactly).wakeBitset(ptr, max, bits); existingwake(ptr, max)becomeswakeBitset(ptr, max, ~0).if (node.wake_bits & bits != 0)before dequeue + signal.Applied to
GroupCOMPLETEnow,FAILED/CANCELEDlater.asyncWaitregisters interestCOMPLETE;unregisterGroupTaskwakes withCOMPLETEon counter→0.fail_fastearly-wake:setFailed/setCanceledwake withFAILED/CANCELED. Completion-only waiters are no longer woken by failure events, and vice-versa — the premature-wake caveat from Support select() and wait() on Group #551 dissolves.Why bitset and not "value re-check at wake"
There's an alternative: have the wake loop re-read the word and re-evaluate a per-node masked-value predicate (
(word & mask) == target). That handles "wake blindly / on every finish," but it is not a standard Linux primitive, and it adds an acquire-load plus ordering reasoning inside the wake loop. Since our wakers always know which event they're signaling, the bitset approach is cheaper (no load in the wake loop), matches the real Linux semantics, and composes better. Prefer bitset.General usefulness
This isn't group-specific. A conditional wake keyed by interest bits is broadly useful for anyone building sync primitives on the futex:
Scope / non-goals
WAIT_BITSET/WAKE_BITSETsemantics via an interest mask on the waiter + an event mask on awakevariant.FUTEX_WAKE_OP(atomic modify + compare on a second word) andFUTEX_CMP_REQUEUE(condvar-style requeue). No current use case.Contract note
Bitset moves a small, explicit obligation onto wakers: if you flip a bit someone can wait on, you must issue a
wakecarrying that bit. That's easier to get right than a value-re-check's ordering, but worth documenting alongside the API.Refs: #551,
src/sync/Futex.zig,src/group.zig.