Skip to content

Futex: bitset-based conditional wake (à la FUTEX_WAIT_BITSET / FUTEX_WAKE_BITSET) #552

Description

@lalinsky

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 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 because unregisterGroupTask 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.

Sketch

  • FutexWaiter gains wake_bits: u32 = ~0 (all-ones default preserves current wait()/prepareWait() semantics exactly).
  • New wakeBitset(ptr, max, bits); existing wake(ptr, max) becomes wakeBitset(ptr, max, ~0).
  • Wake loop adds if (node.wake_bits & bits != 0) before dequeue + signal.

Applied to Group

  • Define event bits: COMPLETE now, FAILED / CANCELED later.
  • 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.

Refs: #551, src/sync/Futex.zig, src/group.zig.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions