C/C++ Arena

Step 7 of 7

Memory ordering

Every atomic operation so far used the default ordering, std::memory_order_seq_cst ("sequentially consistent"). It's the easiest to reason about: all threads agree on one single order of every seq_cst operation. For most code that's the right choice. But to read real concurrent libraries, and to write the fastest code, you need to know the weaker orderings too.

Why ordering is a question at all

To go faster, compilers and processors reorder ordinary memory accesses. A processor may let a later read go ahead of an earlier write that's still sitting in its store buffer, and a compiler may move a write past an unrelated one. Inside one thread you can never tell. Between threads you can: another thread may see your writes in a different order than you made them. Atomic operations with an ordering are how you forbid the reorderings that matter.

Release and acquire: publishing data

The most common pattern: one thread prepares some ordinary data, then sets a flag; another waits for the flag, then reads the data.

#include <atomic>
#include <iostream>
#include <string>
#include <thread>

std::string message;                // ordinary, non-atomic data
std::atomic<bool> ready{false};

int main() {
    std::thread producer([] {
        message = "config loaded";                        // 1. write the data
        ready.store(true, std::memory_order_release);     // 2. publish it
    });
    std::thread consumer([] {
        while (!ready.load(std::memory_order_acquire)) {} // 3. wait for the flag
        std::cout << message << "\n";                     // 4. sees what step 1 wrote
    });
    producer.join();
    consumer.join();
}
config loaded

Relaxed: atomic, nothing more

memory_order_relaxed keeps the operation itself indivisible, but orders nothing around it. It's right for a statistics counter that other threads read only after join() (which synchronizes by itself): hits.fetch_add(1, std::memory_order_relaxed);. It's wrong for anything that signals "the data is ready".

There's also memory_order_acq_rel, for read-modify-write operations such as exchange or compare_exchange that both take and publish, as a lock does.

How to use this in practice

Your turn: build that lock. A spinlock is the simplest possible mutex: an atomic flag that a thread keeps trying to set until it wins. Write try_lock (set the flag with test_and_set using acquire ordering; it succeeds if the flag was clear), lock (retry try_lock until it succeeds) and unlock (clear with release ordering). Real code should use std::mutex, which puts waiting threads to sleep instead of burning a core, but inside every mutex is something like this.

Previous: Condition variables and producer/consumer