liveBook · Manning

Memory ordering

As explained in C++ Concurrency in Action, Second Edition

The constraints governing how memory operations are ordered and observed across threads in the C++ memory model.

Atomic operations make individual reads and writes atomic, but their memory ordering determines what other operations a thread can safely observe. A `std::atomic` operation accepts an optional `std::memory_order` argument; without one, it uses `memory_order_seq_cst`, the strongest default. Choosing a weaker order requires identifying the relationships the program actually needs. section 5.2.1 section 7.2.5

The six ordering values form three broad models:

  • `memory_order_seq_cst` gives sequentially consistent ordering: the atomic operations fit a single total order. It is generally easier to reason about. section 5.3.3
  • `memory_order_relaxed` preserves atomicity without establishing the cross-thread ordering needed to publish earlier work. Seeing a relaxed flag does not, by itself, guarantee seeing a preceding relaxed write to another object. section 5.3.3
  • `memory_order_release`, `memory_order_acquire`, `memory_order_acq_rel`, and `memory_order_consume` belong to the acquire-release model. A release followed by a matching acquire can establish the relationship needed to observe work done before the release; unlike sequential consistency, acquire-release does not impose one total order. section 5.3.3

Publishing a value with a flag

Suppose one thread stores `true` to atomic `x`, then stores `true` to atomic `y`. Another thread waits until it reads `true` from `y`, then reads `x`. If every operation is relaxed, that final read is not guaranteed to see `true`. Make the store to `y` a release and the successful load from `y` an acquire: the earlier store to `x` is then ordered before the later read, even if both operations on `x` remain relaxed. section 5.3.3

Listing 5.8. Acquire-release operations can impose ordering on relaxed operations
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x_then_y()
{
    x.store(true,std::memory_order_relaxed);           #1
    y.store(true,std::memory_order_release);           #2
}
void read_y_then_x()
{
    while(!y.load(std::memory_order_acquire));         #3
    if(x.load(std::memory_order_relaxed))              #4
        ++z;
}
int main()
{
    x=false;
    y=false;
    z=0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();
    assert(z.load()!=0);                              #5
}

The listing shows the flag handoff: release and acquire on `y` provide the ordering that the relaxed operations on `x` cannot provide alone. section 5.3.3

Ordering choices also depend on the operation. Stores permit relaxed, release, or sequentially consistent ordering; loads permit relaxed, consume, acquire, or sequentially consistent ordering. Read-modify-write operations have all six options. Compare-exchange may specify separate success and failure orders: failure performs no store, so its order cannot be release or acquire-release, or stronger than the success order. section 5.2.1 section 5.2.3

For a lock-free stack, the question is concrete: can a popping thread safely read a node’s non-atomic `next` field? The successful push must release the constructed node, and the pop must acquire it before that read. A failed compare-exchange merely retries, so relaxed ordering suffices on that path. Start with the required happens-before relationship, then choose the least restrictive orders that establish it; weaker ordering can improve performance on some architectures, but cannot replace a required synchronization edge. section 7.2.5 section 5.3.3

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Works through contrasting ordering models and a lock-free stack’s success and retry paths, including the relationships each path requires.
Examples
Contrasts two-flag thread examples with a release-acquire flag handoff and a reference-counted lock-free stack.
What it assumes
Assumes familiarity with C++ atomic operations and threads while developing their cross-thread ordering relationships through small examples.
What you can do after
Apply it via `std::atomic` memory-order arguments by deriving required happens-before relationships before weakening success or retry paths.
How it connects
Connects atomic operations to synchronizes-with, happens-before, compare-exchange, release sequences, and lock-free data structures.

Also explained in

Compare how 2 books explain memory ordering →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.