Quant Memo
Core

The C++ Memory Model and Atomics

When multiple threads share memory, the compiler and CPU are both free to reorder operations for speed, and atomics with an explicit memory ordering are how C++ tells them which reorderings are safe to skip.

Prerequisites: Custom Allocators and Memory Pools

A single-threaded program only ever cares about one thing: does the final result match what the source code says, in order? Compilers and CPUs both exploit this freedom aggressively, reordering instructions, caching values in registers, and delaying writes to memory, as long as the single thread's observable behavior looks unchanged. That freedom becomes dangerous the moment a second thread is reading the same memory, because it can now observe operations happening in an order that never appeared in the source code at all.

The C++ memory model is the set of rules that says which reorderings are and aren't allowed when multiple threads touch shared memory, and std::atomic is the tool that lets a programmer constrain those reorderings explicitly. An atomic operation is guaranteed to be indivisible — no other thread ever sees it half-done — and it can carry a memory ordering that controls how much reordering the compiler and CPU are still allowed to do around it.

Without synchronization, "thread A writes X then Y" gives thread B no guarantee it sees X before Y, or even that it sees either write promptly at all — the compiler and CPU are both free to reorder or delay them. std::atomic with the right memory ordering is a contract that forces a specific write to become visible to other threads in a specific order, which is the actual mechanism underneath every lock-free data structure.

A race the compiler is allowed to create

thread A (writer) data = 42 ready = true thread B (reader) if (ready) use(data) without ordering, B can see ready=true before data=42 is visible release/acquire on "ready" forbids exactly that reordering
Plain writes give no cross-thread ordering guarantee; release/acquire semantics on the flag forces "data" to become visible before "ready" does.

Worked example

std::atomic<bool> ready{false};
int data = 0;

// thread A
data = 42;
ready.store(true, std::memory_order_release);

// thread B
while (!ready.load(std::memory_order_acquire)) { /* spin */ }
assert(data == 42);   // guaranteed to hold

memory_order_release on the store guarantees that every write thread A did before the store (here, data = 42) becomes visible to any thread that later sees the store via a matching memory_order_acquire load. Without those orderings — say, if ready were a plain bool instead of an atomic — the compiler would be free to reorder the two writes in thread A, or the CPU could make ready's new value visible to thread B before data's new value is, and the assert could fail nondeterministically, often only under specific compiler optimization levels or CPU architectures, which makes it a nightmare to reproduce.

What this means in practice

Every lock-free queue, spinlock, and shared flag in a trading system's hot path rests on exactly this mechanism. memory_order_seq_cst (the default for std::atomic operations) is the safest and easiest to reason about, since it forbids reordering across all threads consistently, but it's also the most expensive, requiring a full memory fence on many architectures. memory_order_relaxed gives up ordering guarantees entirely in exchange for speed — useful only for things like simple counters where no other memory depends on the order of updates.

Data races on non-atomic shared memory are undefined behavior in C++, not merely "might occasionally give a stale value." The compiler is permitted to assume no data races exist and optimize accordingly, which means a genuine race can produce results far stranger than a stale read — including code that behaves correctly in a debug build and corrupts memory in an optimized one. Any variable touched by more than one thread without synchronization needs to be an atomic or protected by a lock; there is no safe middle ground.

Related concepts

Practice in interviews

Further reading

  • Williams, C++ Concurrency in Action (ch. 5, 'The C++ Memory Model')
ShareTwitterLinkedIn