A traditional futex wait names one futex word. That maps cleanly to a mutex or condition whose blocking state is represented by one shared 32-bit value. Some synchronization designs instead need a thread to sleep until any member of several independent states changes.

futex_waitv() provides that vector wait. The caller supplies an array of wait descriptors, each containing a futex address and an expected value. The kernel checks the vector and blocks only while every entry still matches its expected state.

Each vector entry carries its own expected value

A wait descriptor associates an address with the value that justifies sleeping:

struct futex_waitv waiters[2] = {
    {
        .val = state_a,
        .uaddr = (uintptr_t)&futex_a,
        .flags = FUTEX2_SIZE_U32 | FUTEX2_PRIVATE,
    },
    {
        .val = state_b,
        .uaddr = (uintptr_t)&futex_b,
        .flags = FUTEX2_SIZE_U32 | FUTEX2_PRIVATE,
    },
};

The expected-value check closes the familiar futex race between observing state in userspace and entering the kernel to sleep. If any futex no longer equals its supplied value, futex_waitv() fails immediately with EAGAIN instead of sleeping on stale state.

That property is essential for a vector. The caller is not asking the kernel to wait for arbitrary activity; it is saying that sleep remains valid only while all listed predicates still hold.

One wake identifies one vector position

When the wait completes successfully, the return value identifies the index of a futex that caused the wake. A caller can use that index as a hint about which state deserves attention first.

waiters[0] -> queue state
waiters[1] -> shutdown state
waiters[2] -> configuration state

return 1 -> inspect shutdown state first

The index does not replace rechecking shared state. Futex wakeups are synchronization notifications, while application correctness still depends on atomic state transitions and the surrounding memory-ordering protocol. Another thread may change state again before the woken thread runs.

The safe pattern remains state-driven: load the relevant atomics, decide whether progress is possible, build expected values for the blocked case, wait, then reevaluate after return.

A mismatch prevents sleeping

Suppose a worker observes two queues as empty and prepares a vector wait. A producer can publish work after that observation but before the system call.

Without an expected-value check tied to entering the wait, the worker could miss the transition and sleep despite available work. futex_waitv() compares each futex word against the corresponding val before blocking. A mismatch returns EAGAIN, sending the worker back to its userspace state check.

userspace observes A=4, B=9
          |
producer changes B=10
          |
          v
futex_waitv expects A=4, B=9
          |
          `--> EAGAIN, do not sleep

This is the same core invariant that makes single-word futex waiting useful, extended across a set of candidate wake sources.

Timeout is an absolute deadline

futex_waitv() accepts an optional timespec deadline and a clock identifier. Linux supports CLOCK_MONOTONIC and CLOCK_REALTIME for this interface.

The timeout is a deadline measured against the selected clock rather than a relative sleep duration. That matters when a caller retries after EAGAIN: it can reuse one absolute deadline without subtracting elapsed time on every loop.

CLOCK_MONOTONIC is appropriate when the deadline represents elapsed runtime and should not follow wall-clock adjustments. CLOCK_REALTIME ties the deadline to the system real-time clock instead.

Vector waiting does not merge synchronization objects

The futex words remain independent. futex_waitv() does not combine their ownership rules, establish a total order among them, or perform an atomic application-level transaction across the represented states.

It changes the blocking primitive: one thread can register interest in several futex conditions with one wait. The application still defines what each word means and how transitions are published.

This distinction is important for lock composition. Waiting for either of two locks to change is not equivalent to atomically acquiring both. A wake only gives the thread another opportunity to inspect and attempt its protocol.

Per-entry flags describe futex representation

Each wait descriptor has its own flags. The size flag states the futex word representation expected by the interface, and process-private operation can be declared when the futex is used only among threads of one process.

Reserved fields must remain zero. Treating reserved space as application metadata risks incompatibility with later kernel extensions.

The vector also has a bounded number of entries. Large dynamic dependency sets therefore need a higher-level strategy rather than assuming one syscall can represent an unlimited wait set.

Vector waits reduce the need for synthetic aggregation

Without a multi-futex wait, a design that needs “A or B changed” often introduces another shared word that aggregates notifications. Every producer then has to update or wake that common object correctly in addition to its primary state.

futex_waitv() can remove that synthetic notification point when the real conditions already live in separate futex words. The kernel can block the waiter against the vector directly.

The benefit is structural rather than magical. Producers still perform their normal state transitions and wakes, consumers still recheck state after waking, and memory ordering still belongs to the userspace synchronization design. The system call simply makes waiting on several existing futex conditions a first-class operation.

References