October 11, 2026·4 min read·engineeringcconcurrencystandards

The failure semantics behind proposed atomic waits for C

N3961 proposes atomic waiting for C and asks how much of the underlying operating system behavior the interface should expose.

Waiting for an atomic value to change sounds like a small library addition. The difficult part is deciding what a caller learns when the operating system cannot complete the wait. Niall Douglas addresses that question in N3961, dated August 26 and listed in the WG14 document log on October 11. It proposes C equivalents of the C++20 waiting functions, plus lower-level operations with explicit failure reporting and optional time bounds.

C remains important to me because so many language boundaries and platform interfaces depend on it. That makes this proposal interesting beyond programs written entirely in C. An interface at this level has to say what the operating system can do and what the library promises on top. Hiding a failure transfers responsibility to the library, which then needs a defensible policy for handling it.

Why polling became a library problem

The earlier C++ paper P0514R4, dated May 3, 2018, describes the cost of atomic polling under contention. It names poor performance when runnable work exceeds available execution capacity, and energy consumption even without that oversubscription. Its proposed waiting functions let existing atomic algorithms gain blocking support without a wholesale change in their representation.

The paper also acknowledges an implementation cost. Supporting arbitrary atomic objects can require a library-owned semaphore table. Unrelated updates can interfere through shared entries in that table. A portable interface therefore does not imply that every atomic type maps directly to the same operating system mechanism.

That is a useful way to think about the abstraction. A program asks to wait on its own state. The library must map that state to whatever the platform can block on. The efficiency of that mapping depends on properties that a generic type signature can conceal. Exposing a convenient operation and choosing an efficient representation are related design tasks.

Linux supplies an atomic comparison before sleeping

The Linux futex manual describes a 32-bit word with four-byte alignment. A blocking operation compares that word with the expected value and sleeps only if the comparison succeeds. Loading, comparing and blocking occur atomically with respect to concurrent futex operations on the same word.

That atomic comparison prevents a particular lost notification. Consider a waiter that has read an unavailable state, while another thread makes the resource available. If the second thread signals before the first actually sleeps, an unconditional sleep could miss the useful change. Comparing again as part of entering the blocked state closes that gap.

The manual describes the uncontended lock path as user-space atomic instructions. The kernel becomes involved when a thread needs to block. It also documents explicit errors, including an invalid address or alignment. This division explains the attraction of waiting on an address. The representation of availability already exists in memory, and the blocking mechanism can refer to that representation.

Windows makes early returns part of the contract

Microsoft's WaitOnAddress documentation accepts values of one, two, four or eight bytes. The function returns immediately if the current value differs from the comparison value. It has a millisecond timeout argument and reports failure through its return value and GetLastError.

The same documentation explicitly permits early returns for reasons other than the desired state change. Callers must compare the value again. A low-memory condition is among the documented reasons for an early wake. The returned control flow therefore does not establish that the application's condition is satisfied.

WakeByAddressAll wakes threads waiting on the address within the same process. It returns no value. That process boundary is part of the platform contract, even though the function names describe a general operation on memory addresses.

These details matter when evaluating a standard library wrapper. Two platforms can both support waiting on an address while differing in accepted sizes, error reporting and scope. A portable contract needs its own precise description. Copying one platform's name or argument list does not establish that description.

The application still owns the state protocol

P0514R4 explicitly notes that a transient value can go unobserved. A value can change and then return to its original state before a waiter observes it. This is the ABA problem in the context of waiting. Waiting on a value consequently cannot be treated as a history of every update to that value.

The practical implication follows from what the interface observes. If an application needs to count every event, a state that returns to its old value cannot by itself represent that requirement. The state representation has to preserve the distinction the application cares about. A notification is useful for scheduling a waiter, but the meaning of the work remains in the application's protocol.

This is where I prefer explicit interfaces in low-level code. A caller should know whether it is waiting for a persistent condition or trying to detect an occurrence. Those requirements lead to different representations. A library can provide blocking and memory-order guarantees without deciding which events the application is allowed to forget.

N3961 keeps the unresolved recovery question visible

Douglas proposes atomic_wait_expected and atomic_notify, using a native waiting type of at least 32 bits. The wait reports the observed value, and the notify combines compare-exchange with waking waiters. The paper leaves open how callers should recover if the update succeeds but notification fails. Retrying the same comparison alone need not repeat the wake.

That case deserves a first-class explanation in the eventual interface. The application has already changed shared state, so an error cannot simply mean that nothing happened. Recovery needs to account for the completed update and any threads that remain asleep. A generic retry policy cannot infer the right application action from the function name.

The proposal remains committee work, including questions about permitted memory orders. Its value at this stage is the concrete choice it puts before WG14. C can gain familiar waiting operations while also considering interfaces that make time limits and partial failure visible. The recovery semantics will determine how those operations fit into synchronization protocols that need to keep making progress after a platform error.