5.2.27 · D5C++ Programming
Question bank — Memory model — happens-before, acquire-release semantics
Before you start, a two-word glossary so every trap below is readable from line one:
True or false — justify
Making a flag std::atomic<bool> automatically protects the plain int data you wrote before it.
False. Atomic only stops torn reads of the flag itself; the plain
data is ordered solely by the happens-before edge, which needs release/acquire, not mere atomicity. See std-atomic and atomic operations.memory_order_relaxed on both store and load still guarantees no data race on the atomic variable.
True for the atomic itself — relaxed is still atomic, so the atomic never tears — but false for any surrounding non-atomic data, which gets no ordering and races.
An acquire-load orders the memory operations that come before it in program order.
False. Acquire is a one-way upward fence: it stops later operations from moving above it. Earlier ops are not its concern.
A release-store orders the operations that come after it in program order.
False. Release is a one-way downward fence: it pins earlier ops above it; later ops may still float up past it.
acq_rel gives every thread a single, globally consistent order of all atomic operations.
False.
acq_rel only orders memory along a specific synchronizes-with chain. A global total order requires seq_cst; see Sequential consistency vs weak memory models.If Thread 2's acquire-load reads an older value than the one Thread 1's release-store wrote, they still synchronize-with each other.
False. The edge exists only when the acquire actually reads the released value (or a later value in its release sequence). Reading an older value means the handshake never completed.
Happens-before is transitive.
True. If and then — that transitivity is exactly what lets a release/acquire pair carry along everything sequenced before it.
Two threads writing the same non-atomic variable with a happens-before edge between them is a data race.
False. A happens-before edge orders the two accesses, so it is not a race. Data races require the absence of ordering.
Locking a mutex behaves like an acquire, and unlocking it like a release.
True. Lock acts as acquire, unlock as release, so critical sections publish their writes to the next thread that locks.
seq_cst is always the correct default choice when unsure.
True in spirit — it is the safest and the language default — but it is often slower, paying for a global total order with extra fences. Correctness first, then relax deliberately.
Compiler reordering and CPU reordering are two names for the same phenomenon.
False. They are distinct layers: the compiler reorders under the as-if rule at build time; the CPU reorders at run time via out-of-order execution and store buffers. A memory tag must defeat both.
Within a single thread, reordering can ever be observed by that same thread.
False. The as-if rule guarantees single-thread execution appears in program order; reordering is only observable across threads.
Spot the error
A programmer writes data = 42; then ready.store(true, relaxed); and expects the consumer to see data == 42.
Error:
relaxed forms no synchronizes-with edge, so there is no happens-before between data = 42 and the consumer's read — the assert may fire. Use release/acquire.A consumer does ready.load(release) to read a flag.
Error:
release is a store ordering; a load cannot be release. A load must use acquire (or relaxed/seq_cst). The compiler will reject or the semantics are meaningless.Someone uses acquire on a fetch_add read-modify-write and expects their prior writes to be published.
Error: a RMW that must both publish prior writes and consume others' writes needs
acq_rel. Pure acquire gives no release side, so earlier writes are not pinned above it.A team relies on the time gap between two threads ("Thread 1 ran first, so its writes are surely visible") to skip acquire/release.
Error: synchronization is about data flow, not wall-clock time. Without the value travelling writer→reader via an atomic, there is no ordering no matter who "ran first".
Code uses relaxed for a reference-count decrement, then immediately deletes the object when the count hits zero — with no fence.
Error: the final destructor must see all prior threads' writes, which needs an
acquire (or acq_rel) on the decrement that reaches zero, else deletion races with those writes.A busy-wait loop while(!ready.load(relaxed)); is used, then the guarded data is read.
Error: the loop terminates correctly (relaxed still updates modification order) but forms no hb edge, so the following data read may be stale. The load must be
acquire.Why questions
Why does the memory model demand the acquire-load read the exact stored value, not just any value?
Because ordering is carried by data dependency: only when the published value physically travels from writer to reader can the reader inherit the writer's prior work. A different value means it observed a different (or no) publication.
Why is seq_cst strictly stronger than acq_rel even though both include acquire and release?
acq_rel only orders along individual synchronizes-with chains; seq_cst additionally forces one global total order all seq_cst ops agree on, defeating the IRIW/Dekker inconsistency that acq_rel permits.Why can two independent reader threads disagree on the order of two independent acq_rel flag writes?
Because
acq_rel gives no cross-flag global order — each reader may observe the two stores propagate through caches in a different sequence. Only seq_cst forbids this (the IRIW case).Why does a data race make the whole program undefined behavior rather than just producing a wrong value?
The standard defines racing accesses as UB so compilers may assume they never happen and optimize freely; once assumed-impossible behavior occurs, any outcome (crash, silent corruption) is permitted.
Why is a plain bool ready = true flag insufficient even if the CPU happens to be strongly ordered today?
The compiler may still reorder or cache the plain write under the as-if rule, and portable code must survive weak-memory CPUs. Correctness must come from the model, not from a lucky platform.
Why does release act "downward" and acquire act "upward" rather than both directions?
A publisher only needs its earlier work pinned below the signal (downward); a subscriber only needs its later reads pinned after seeing the signal (upward). One-way fences are cheaper and exactly sufficient for message passing.
Why can relaxed still be useful despite giving no ordering?
For counters and statistics where only atomicity matters (no surrounding data must be ordered),
relaxed avoids fence cost — a staple of Lock-free programming patterns.Edge cases
If the acquire-load never observes the released flag (spins forever), does any happens-before edge exist?
No. With no successful read of the stored value, no synchronizes-with edge forms, so nothing is published — the reader would see nothing even if it later stopped.
What happens-before guarantees exist when both operations are on the same thread using relaxed?
Full sequenced-before ordering still holds intra-thread — relaxed never weakens single-thread program order; the weakness is purely cross-thread.
Is there any ordering between two relaxed stores to the same atomic from different threads?
Yes — every atomic has a single modification order, so all threads agree on the sequence of writes to that one variable, even under relaxed. What they don't get is ordering of surrounding memory.
Does an acquire fence with no matching release fence do anything useful?
On its own, no synchronizes-with edge forms; a standalone fence only helps when paired with a release on the other side and an atomic load/store linking them.
If data is itself std::atomic and read/written with relaxed on both sides, is the message-passing example still broken?
The data no longer races (it is atomic), but the consumer may still read a stale
data value because relaxed gives no ordering with the flag — logically wrong, though not UB.What is the ordering guarantee at the very first operation of a thread with respect to the thread that spawned it?
Thread creation establishes a happens-before edge: everything sequenced-before the
std::thread construction happens-before the new thread's first operation, so the child sees the parent's prior writes.Recall One-line summary of every trap
Ordering comes from data flow through the right tag (release publishes, acquire subscribes, the value must travel), never from time, never from "atomic ≈ safe", and acq_rel is local while only seq_cst is global.