July 5, 2018·6 min read·engineeringcstandardsconcurrencythreads

C17 makes unsuccessful synchronization less ambiguous

C17's publication consolidates corrections to C11. Its mutex and condition-variable resolutions explain why failed acquisition and a returned wait do not establish application state.

A failed attempt to acquire a mutex does not prove that another thread holds it. A condition-variable wait that returns does not prove that another thread signaled it. Those limitations are explicit in the correction work consolidated by C17, whose ISO lifecycle records publication on July 5. The edition's publication label is June 2018, but the lifecycle gives the separate day on which the publication stage completed.

The relevant history appears in WG14's clarification summary. DR 470 addresses spurious mtx_trylock failure, and DR 480 addresses unspecified reasons for condition-variable waits to unblock. Both entries are marked for C17. They concern what the application may infer from an operation's result, which is a more consequential question than whether an implementation recognizes the function name.

C's undefined-behavior boundaries matter to me because low-level code has to remain within the language's actual contract. Thread interfaces make that requirement less intuitive. A return value that looks like evidence about another thread may establish no ordering that permits the caller to read that thread's ordinary data.

Busy does not identify a successful competing acquisition

DR 470 starts from a mismatch between an intuitive interpretation of thrd_busy and the synchronization rules. The original return description associated that result with a resource already in use. The correction explicitly permits mtx_trylock to fail to acquire an unused resource and requires it to return thrd_busy in that case.

The implementation discussion gives a hardware reason. On architectures using a load-and-reserve followed by a conditional store, an intervening scheduling event can cause the reservation to be lost. The store can then fail even though another thread did not acquire the mutex. An implementation based on a weak atomic compare-exchange can expose that failure through the try-lock interface.

Allowing that result avoids requiring the implementation to turn every weak attempt into a stronger acquisition protocol. The report also explains why giving failed acquisition additional synchronization semantics would have a cost. It discusses ARM and PowerPC implementations where an acquisition transition would need stronger memory ordering if it also had to support the proposed inference from failure.

The portability rule follows from the permitted outcome. thrd_busy tells the caller that this attempt did not acquire the mutex. It cannot establish the identity, actions or existence of a competing owner. An application needs a separate mechanism if its protocol depends on learning that another thread has completed a particular operation.

Failure is not a substitute for a publication protocol

The report illustrates the problem with an ordinary variable written by one thread before it locks a mutex. Another thread repeatedly tries the mutex, releases it after successful attempts, and eventually observes a failed attempt. It then reads the ordinary variable. The tempting inference is that failure means the writer has taken the mutex and therefore must already have performed the write.

DR 470 states that this program has a data race under C11 independently of the spurious-failure question. The specified synchronizes-with relationships do not make the failed attempt establish the required happens-before relation. Even a hypothetical guarantee against spurious failure would not by itself supply the missing ordering rule.

That is why the correction addresses both implementation latitude and programmer expectations. Permitting failure on an unused mutex makes the limitation visible at the interface. The caller cannot reasonably treat failure as a reliable event notification when the operation is allowed to fail without any competing acquisition at all.

The report distinguishes this misuse from ordinary try-lock applications, including attempts to acquire several locks without deadlocking. Those applications can respond to failure by abandoning or retrying an acquisition attempt. They do not need to interpret failure as publication of another thread's unrelated data. The same result code can be sufficient for one protocol and insufficient for another.

I prefer synchronization explanations that name the relationship they establish. Saying that code uses a mutex is incomplete if the relevant path never successfully acquires it. The corrected result contract gives reviewers a specific question about that path instead of encouraging an inference based on the function's name.

A condition variable can return without a signal

DR 480 concerns the other side of an operation that appears to report an event. Its final recorded wording permits cnd_wait to unblock for an unspecified reason. The corresponding cnd_timedwait wording permits an unspecified reason in addition to notification and expiration of its specified absolute time.

The report explains an implementation consequence. POSIX and C++ condition variables already permit spurious wakeups. Allowing the C interface to expose the same possibility permits a thin wrapper over an existing implementation. Requiring the wrapper to eliminate all such returns could approach the complexity of implementing condition variables again.

The discussion also warns that absence of spurious wakeups would not automatically make a returned wait sufficient proof about shared state. Establishing the relevant ordering between waiting and signaling still needs a protocol. The report notes that cnd_wait is not specified to synchronize directly with cnd_signal. The application's mutex-protected state or another synchronization mechanism must establish the required relationship.

This matters because a notification is an operation on the condition variable, while readiness is a property of the application. A protocol can use notification to make a waiting thread runnable, but the application still needs a representation of the condition that makes continued execution valid. The two concepts cannot be collapsed into the return of one library call.

The predicate belongs in protected state

A minimal waiting fragment can make the distinction explicit. Here ready is application state, and the caller already holds mutex. The surrounding program must initialize the synchronization objects and update ready under the same protection:

/* Fragment within a function, with mutex already locked. */
while (!ready) {
    if (cnd_wait(&changed, &mutex) != thrd_success) {
        /* The surrounding function handles the wait failure. */
        break;
    }
}

This fragment deliberately leaves error handling to the enclosing function. Leaving the loop because a wait failed does not establish ready, so subsequent code must distinguish that path before using the supposedly available state. The loop's normal condition is a read of the protected predicate. A successful return from the wait merely leads to another evaluation of that condition.

The distinction is relevant even when a particular execution has a corresponding signal. Notification does not replace the state check. The report's analysis requires a happens-before relationship for the memory the application wants to use, and its discussion describes the shared mutex as one way to establish the necessary ordering between critical sections.

The timed interface has the same conceptual separation. An absolute deadline gives the waiting operation an additional permitted reason to return. It does not define what the application predicate means at that moment. The caller must account for the wait result and the protected state according to its own protocol, including the possibility that an operation did not complete successfully.

Correction changes the explanation of familiar operations

The GCC 8.1 standards chapter describes C17 as C11 with corrections integrated and states that GCC applies those corrections in C11 mode as well. Publication should therefore not be read as the first moment every implementation could exhibit the behavior described by these reports. The defects concern the specification and interpretation of existing interfaces.

An implementation built on a platform's existing thread library also has an incentive to preserve that library's permitted outcomes. DR 480 makes this explicit for POSIX wrappers. Requiring a stronger C wrapper would introduce work whose purpose was to hide an outcome that the application already needs a protected predicate to handle correctly. The correction allows the wrapper and application protocol to divide responsibility in the same way as the underlying condition-variable interface. This is a specific compatibility benefit of the wording, independently of any new syntax or additional function.

The history within DR 480 shows why final wording matters. Its initial suggestion retained language about endeavoring to block and described the extra wakeup as spurious. Subsequent committee discussion removed that adjective and changed the operation's description to blocking. The resulting correction still explicitly allows an unspecified reason for unblocking. Reading only the first suggested edit would give an incomplete account of the resolution.

Taken together, the two reports identify different limits on inference. A failed try-lock does not establish a competing owner's activity. A returned condition-variable wait does not establish a notification or the application's condition. Successful synchronization and protected state remain the evidence that the application must use when deciding whether it can safely access shared data.