The question this answers
What does a memory barrier actually constrain, and why is "flush the cache" the wrong mental model?
A ring-buffer producer writing an entry into slot i and then publishing it by bumping the write index.
The ring slots and the atomic write index.
If a consumer observes writeIndex > i, then slot i is fully written and safe to read.
Synchronization exists to preserve this sentence. If a schedule can make it false, the code is wrong no matter which primitive it uses.
The orderings, and what each one constrains
Start with the correction, because it prevents a whole family of wrong arguments. On the machines you are writing for, caches are *already coherent*: the hardware guarantees that all cores eventually agree on the value of any location, without software doing anything. A barrier does not push data out of a cache and does not make anything visible that was going to stay invisible. What it constrains is the order in which *this core's* memory operations become visible relative to each other.
With that fixed, the orderings are learnable as a small set. relaxed: atomic, orders nothing. acquire on a load: nothing after it may move before it. release on a store: nothing before it may move after it. acq_rel on a read-modify-write: both. seq_cst: all of the above, plus a single total order over all sequentially consistent operations that every thread agrees on.
Acquire and release are deliberately one-way. That is what makes them cheaper than a full fence and exactly sufficient for publication: the producer needs its prior writes not to sink past the publish, and the consumer needs its later reads not to hoist above the observe. Neither needs the other direction, and paying for it is a real cost on architectures that implement it with an instruction.
| Ordering | What may not move across it | Total order across threads? | Typical use |
|---|---|---|---|
| relaxed | Nothing. The operation is atomic and orders nothing else | No | A statistics counter nothing else depends on |
| acquire (loads) | Later operations may not move before it | No | The consumer half of a publication |
| release (stores) | Earlier operations may not move after it | No | The producer half of a publication |
| acq_rel (RMW) | Both directions, on one read-modify-write | No | A CAS that both consumes and publishes |
| seq_cst | Both directions, plus a single total order all threads agree on | Yes | The default; correct by construction, most expensive where fences are needed |
| standalone fence | Both directions at that point, not attached to any one location | Depends on the fence | Rare; almost always the wrong tool versus an ordered atomic |
The publication pair on a ring buffer
The ring buffer is the cleanest place to see why one release and one acquire are exactly what is required. The producer writes an arbitrary amount of data into slot i — a whole struct, several fields, a memcpy — and then publishes with a single release store of the index. The consumer acquires the index and, if it is greater than i, may read the slot.
None of the slot fields is atomic and none needs to be. The release/acquire pair covers all of them by transitivity, which is the same economy described in Happens-Before: The Edge That Makes a Write Visible. This is the pattern behind every SPSC queue, every lock-free logging buffer and every shared-memory ring between processes, and getting the pair right is essentially the entire correctness argument.
Note what the barrier does *not* do here. It does not make the slot writes happen sooner, it does not push them anywhere, and it does not prevent the consumer from reading a slot it was never entitled to read. It says only: if you observed my index, you will also observe everything I wrote before publishing it.
1struct Entry { uint64_t ts; uint32_t level; char msg[96]; };2 3Entry ring[CAP];4std::atomic<uint64_t> writeIndex{0}; // the ONLY atomic here5 6// ---- producer ----7void publish(const Entry& e) {8 uint64_t i = writeIndex.load(std::memory_order_relaxed); // sole writer9 ring[i % CAP] = e; // plain writes: ts, level, 96 bytes10 11 // RELEASE: none of the writes above may move after this store.12 // That single constraint publishes the whole struct.13 writeIndex.store(i + 1, std::memory_order_release);14}15 16// ---- consumer ----17bool consume(uint64_t i, Entry& out) {18 // ACQUIRE: no read below may move above this load.19 if (writeIndex.load(std::memory_order_acquire) <= i) return false;20 21 out = ring[i % CAP]; // guaranteed to see the producer's22 return true; // writes to this slot23}24 25// NOTE: this is the ordering argument only. A real ring also needs a26// read index, wraparound protection and overwrite handling.What relaxed gives you, and what it does not
The most instructive failure is the one where every access is atomic and the invariant still breaks. Make both the slot and the index relaxed atomics: every read returns some value that was actually written, nothing tears, a race detector reports nothing — and the consumer can still observe the new index with the old slot contents, because relaxed constrains nothing but atomicity.
This is the concrete meaning of "atomic does not imply ordered", and it is worth having seen once as a schedule rather than as a sentence. The step marked below is legal: relaxed atomics permit exactly this observation, on any architecture whose hardware permits the corresponding reordering.
The practical rule that follows: start with seq_cst, which is the default in C++ and in JavaScript's Atomics.*, and weaken only with a specific argument about which edge you still need and a measurement showing the stronger ordering cost something. Relaxed is correct for a counter nothing is ordered against and is a bug in every publication pattern.
| # | Producer | Consumer | State |
|---|---|---|---|
| 1 | relaxed store ring[0].level = 3 | · | C sees ring[0].level=0 C sees writeIndex=0 |
| 2 | relaxed store ring[0].ts = 1699999999 | · | C sees ring[0].ts=0 C sees writeIndex=0 |
| 3 | relaxed store writeIndex = 1 | · | C sees writeIndex=0 |
| 4 | · | relaxed load writeIndex -> 1 | C sees writeIndex=1 |
| 5 | · | relaxed load ring[0].ts -> 0 | C sees ring[0].ts=0 ✕ The consumer observed writeIndex == 1 and read an unwritten slot. Every access was atomic; none was ordered. This is the difference between atomicity and ordering, in one step. |
| 6 | [with release on writeIndex, acquire on the load] store writeIndex = 1 | · | C sees ring[0].ts=1699999999 |
Key points
- A barrier constrains the order in which this core's memory operations become visible. It does not flush caches — caches are already coherent.
- Acquire and release are one-way: nothing after an acquire moves before it, nothing before a release moves after it. That asymmetry is why they are cheap and sufficient.
- One release plus one acquire publishes an arbitrarily large amount of plain data, by transitivity.
- Relaxed gives atomicity and no ordering, so a fully relaxed publication is broken while looking fully synchronized.
- Start at seq_cst and weaken only with a specific edge argument plus a measurement. The reverse order produces subtle bugs for unmeasured gains.
The loop, answered
Every field is required, which is why no lesson here can recommend concurrency without naming the interleaving that breaks it, the complexity it adds, and the simpler thing to consider first.
- • The ordering constraint attached to an atomic operation tells the compiler which movements across it are forbidden.
- • The compiler emits whatever the target needs to obtain the same constraint from the hardware — nothing at all on some targets, a dedicated instruction or an explicit fence on others.
- • A release store means: every memory operation sequenced before it in this thread is visible to any thread that acquires this value.
- • An acquire load means: every memory operation sequenced after it in this thread happens after whatever the releasing thread published.
- • Sequential consistency additionally places all such operations in one total order that every thread observes identically, which is what makes it the easiest to reason about and the most expensive to provide.
- • Producer writes slot then release-stores the index; consumer acquire-loads the index and reads the slot correctly. No schedule breaks it.
- • All relaxed: consumer observes the new index and an unwritten slot. Atomic throughout, ordered nowhere.
- • Release on the producer, plain load on the consumer: no edge, because an edge is a pair. The consumer may observe the index without the slot.
- • Two producers on the same relaxed index: both compute the same slot and both write it. The barrier question is untouched — this is a mutual-exclusion problem, and barriers do not provide mutual exclusion.
- • seq_cst throughout: correct, and on a weakly ordered target it emits more fences than the acquire/release version needs. Correct-and-slower is the right starting point.
- • Promises: the specified operations may not be reordered across the barrier by the compiler or by the hardware.
- • Promises: with a paired release and acquire, everything the releasing thread did before is visible to the acquiring thread after.
- • Promises (seq_cst only): a single total order over sequentially consistent operations that all threads agree on.
- • Does NOT promise: mutual exclusion. Two threads may execute the same region simultaneously; a barrier orders and does not exclude.
- • Does NOT promise: that anything is "flushed". Coherence already guarantees eventual agreement on a location's value; the barrier orders operations, not caches.
- • Does NOT promise: an edge from one half of a pair. A release with no acquire orders nothing between threads.
- • Does NOT promise: that relaxed atomics are ordered. They are not, and that is their entire specification.
- • Barriers are not themselves contention, but on targets that implement them with instructions they prevent the CPU from overlapping work, which shows up as reduced throughput on the barriered path.
- • seq_cst is the most constrained ordering and costs the most where fences are real instructions; on x86-64 a release store and an acquire load are plain instructions and cost nothing extra, while a seq_cst store needs a fence.
- • The location being ordered is still a shared line, and the coherence traffic on it is a separate cost from the barrier. See What a Shared Write Costs.
- • Relaxed publication — atomic everywhere, ordered nowhere; the flagship failure of this lesson.
- • One-sided pairing — a release with no acquire, or vice versa, producing code that reads as synchronized and orders nothing.
- • Cargo-culted fences — a standalone fence added because a bug went away, ordering something unrelated and leaving the real edge missing.
- • Confusing barriers with mutual exclusion, producing two threads correctly ordered and simultaneously in the same region.
- • Premature weakening — orderings relaxed for performance without a measurement, trading a correctness argument for nothing.
- • Publication patterns: ring buffers, SPSC queues, configuration swaps, lock-free structures — anywhere a small atomic publishes a large amount of plain data.
- • Hot read paths where taking a mutex would dominate, and the read genuinely needs only an acquire.
- • Cross-process shared memory, where a mutex may not be usable at all and the ordering argument is the only tool available.
- • In ordinary application code where a mutex would be correct, obvious and fast enough.
- • When weakened orderings are chosen by intuition rather than measurement, which converts an easy correctness argument into a hard one for no measured gain.
- • When a standalone fence is used in place of an ordered atomic, since the fence is not attached to a location and the argument becomes much harder to check.
- • Verify correctness with a thread sanitizer and on a weakly ordered target before measuring anything. An ordering bug that only appears on ARM will not be found by a benchmark on x86-64.
- • To justify weakening, measure the seq_cst version and the acquire/release version on the real workload and on the target architecture. On x86-64 the difference is frequently zero for loads and stores.
- • Inspect generated assembly on the weakly ordered target: the barrier the source implies should be visible, and its absence means the ordering argument is wrong somewhere.
- • Use a formal tool where the stakes justify it — a memory-model checker such as CppMem or herd7 can exhaustively check small publication idioms in a way testing cannot.
- • For the ring specifically, assert a magic value or a sequence number inside each slot on the consumer side; a mismatch is a visible ordering failure rather than a silent one.
- • Every atomic gains an ordering argument that is part of its contract and must be re-checked whenever surrounding code moves.
- • The argument is not local: it spans the producer and the consumer, which may be in different files owned by different people.
- • Weakened orderings create a second layer of reasoning on top of the first, and the payoff is often architecture-specific.
- • Testing has weak power here, so confidence has to come from review, sanitizers and multi-architecture CI rather than from a passing suite.
- • A mutex, which supplies both ordering and exclusion with no per-operation ordering argument. See Mutexes: What They Protect and What They Do Not.
- • A library ring buffer or concurrent queue whose ordering argument has already been made and reviewed. See Concurrent Queues.
- • seq_cst everywhere as a deliberate default, weakening only where a profile shows a cost. Correct-and-slower is a legitimate engineering position.
- • Message passing or process isolation, where the runtime supplies the edge and the question does not arise. See Message Passing.
- • Immutable publication through a single atomic pointer, which reduces the entire ordering surface to one location and one pair. See Safe Publication: Handing Over a Finished Object.
Both threads read 0, and both wrote first
x = y = 0
Thread 1 Thread 2
x = 1; y = 1;
r1 = y; r2 = x;
Sequential reasoning: one of the two stores must land first,
so at least one load must see a 1. r1 == 0 && r2 == 0 is impossible.| # | Thread 1 | Thread 2 | State |
|---|---|---|---|
| 1 | x ← 1 | · | x=1 y=0 r1=0 r2=0 |
| 2 | r1 ← y | · | x=1 y=0 r1=0 r2=0 |
| 3 | · | y ← 1 | x=1 y=1 r1=0 r2=0 |
| 4 | · | r2 ← x | x=1 y=1 r1=0 r2=1 |
Publish a value, then a flag — which edge makes it visible?
Writer Reader
data = 42; while (ready == 0) { }
ready = 1; use(data);| # | Writer | Reader | State |
|---|---|---|---|
| 1 | data ← 42 | · | data=42 ready=0 reader sees=— |
| 2 | ready ← 1 (plain store) | · | data=42 ready=1 reader sees=— |
| 3 | · | read ready → 1 | data=42 ready=1 reader sees=— |
| 4 | · | read data → 0 | data=42 ready=1 reader sees=0 ✕ the reader observed ready = 1 and data = 0 — it saw the flag that announces the write without seeing the write |
What people believe, and what is true
A barrier flushes the cache so other cores can see my write.
Caches are coherent already; the write will be seen regardless. The barrier constrains the order in which this core's operations become visible relative to each other.
Everything is atomic, so the ordering is fine.
Relaxed atomics order nothing. Atomicity and ordering are independent, and a fully relaxed publication is broken.
A memory barrier gives mutual exclusion.
It orders operations. Two threads can be inside the same region simultaneously with every barrier correctly placed.
Go deeper
Overview
A barrier says which of your memory operations may not cross this point. It is about order, not about pushing data anywhere.
Practical
Publication is one release on the producer and one acquire on the consumer. That pair covers everything written before it, so the payload does not need to be atomic.
Advanced
Weaken from seq_cst only with both halves of a justification: which edge you still need, and a measurement showing the stronger ordering cost something on the target you ship. On x86-64 the load and store cases often cost nothing, so the weakening buys nothing either.
Internals
CPU-SPECIFIC: on x86-64 the fence instructions are mfence, lfence and sfence, and only the seq_cst store path typically needs one. On 64-bit ARM the relevant instructions are LDAR and STLR plus the DMB family. These names are useful for reading disassembly and are not something to write code against.