Quant Memo
Advanced

Memory Barriers and Ordering

Why a CPU is allowed to run your instructions out of the order you wrote them, why that's normally invisible, and the special fences — memory barriers — needed to keep it from breaking multi-threaded code.

Prerequisites: Mutexes, Spinlocks and Contention

Write two lines of code — set a data value, then set a "ready" flag — and it's natural to assume another thread reading the flag afterward will always see the correct data too. On a single core running one thing at a time, that's true. Across multiple cores, it can silently fail, because both the compiler and the CPU are allowed to reorder your instructions, and each core keeps its own local, cached view of memory that doesn't update everywhere at the same instant. Reordering and caching are what make modern CPUs fast — most code never notices — but they mean "the order I wrote it" and "the order another thread observes it" are not the same guarantee.

A memory barrier (or "fence") is an instruction that restricts this reordering at a specific point: it tells the CPU "don't let any memory operation that comes after this line, in program order, become visible to other cores before any operation that comes before it." Barriers come in variants — a store barrier orders writes, a load barrier orders reads — and picking the right one is what "lock-free" and "atomic" programming is actually built on. A mutex is popular precisely because it quietly includes the correct barriers for you; writing lock-free code by hand means taking on the responsibility of placing barriers correctly yourself.

Worked example: a flag that lies without a barrier

Thread A writes data = 42 and then ready = true. Thread B spins on while (!ready) {} and then reads data, expecting 42. Without any ordering guarantee, two independent things can go wrong. First, the compiler or CPU on Thread A's side is free to reorder the two writes — nothing in the source requires data to become visible before ready — so Thread B can observe ready == true while data still holds its old, stale value. Second, even if the writes happen in order on Thread A's core, its updates propagate to Thread B's core through the cache-coherence system, and without a barrier there's no guarantee Thread B's core has picked up the new value of data even after it's picked up ready. The fix is a release barrier on Thread A's write to ready (guaranteeing everything written before it, including data, is visible first) paired with an acquire barrier on Thread B's read of ready (guaranteeing nothing read after it is allowed to be reordered ahead of the read). With that acquire/release pair in place, "Thread B sees ready == true" is guaranteed to imply "Thread B sees data == 42."

Thread A data = 42 release barrier ready = true Thread B read ready == true acquire barrier read data == 42
A release barrier on the writer paired with an acquire barrier on the reader guarantees that seeing the flag implies seeing the data that was written before it — without both, the CPU is free to reorder or delay visibility.

What this means in practice

Memory barriers are the primitive underneath every higher-level concurrency tool: mutexes, atomics with acquire/release semantics, and lock-free queues like an SPSC ring buffer all rely on barriers placed at exactly the right points, usually hidden behind a language's memory-model API rather than raw assembly. Getting them wrong doesn't crash reliably — it produces a bug that appears once in a million runs, only under real multi-core contention, and vanishes when you add a print statement or run under a debugger, because both change the timing enough to hide the race.

CPUs and compilers may reorder memory operations for speed; a memory barrier restricts that reordering at a specific point so that one thread's writes become visible to another thread in a guaranteed order — the mechanism underneath every lock and lock-free structure.

These bugs are notoriously non-reproducible: missing a barrier usually doesn't fail every run, only under specific timing and hardware conditions, and adding debug logging or running under a debugger often changes the timing enough to make the bug disappear. Never conclude code is correct just because it passed testing — reason about the required barriers explicitly.

Related concepts

Practice in interviews

Further reading

  • Gregg, Systems Performance, ch. 6 (CPUs)
  • Preshing, 'Memory Barriers Are Like Source Control Operations' (blog)
ShareTwitterLinkedIn