Skip to content
C++
Library
since C++11
Expert

std::memory_order

Controls how memory accesses around atomic operations are ordered across threads; the backbone of C++'s lock-free concurrency model.

std::memory_ordersince C++11

An enumeration that specifies the visibility and ordering constraints applied to memory operations surrounding an atomic load, store, or read-modify-write, governing how those operations synchronize between threads.

Overview

Modern CPUs and compilers reorder memory accesses for performance β€” a load issued before a store in source code may execute after it at runtime. On a single thread this is invisible, but across threads the effect is observable and catastrophic in lock-free code. std::memory_order gives precise control over which reorderings are forbidden around each atomic operation.

The six values form a spectrum from cheapest to most restrictive:

ValueGuarantees
memory_order_relaxedAtomicity only; no ordering constraints
memory_order_consumeData-dependency ordering on the consuming load
memory_order_acquireNo subsequent loads/stores move before this load
memory_order_releaseNo preceding loads/stores move after this store
memory_order_acq_relAcquire + release semantics on a single RMW
memory_order_seq_cstTotal order across all seq_cst operations (default)

In C++20, std::memory_order became a scoped enum. The unscoped names (memory_order_relaxed, etc.) remain as inline constexpr aliases, so existing code continues to compile unchanged. New code may use std::memory_order::relaxed, std::memory_order::acquire, etc.

Synchronizes-with

The central relationship is synchronizes-with: a release store on thread A synchronizes-with an acquire load on thread B that reads the stored value. Everything thread A wrote before the release is guaranteed visible to thread B after the acquire. This is how data ownership transfers between threads without a mutex.

Syntax

cpp
#include <atomic>

// C++11: unscoped enum (names remain valid in all later standards)
enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

// C++20: scoped enum; unscoped names become inline constexpr aliases
namespace std {
    enum class memory_order : /* unspecified */ {
        relaxed, consume, acquire, release, acq_rel, seq_cst
    };
    inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
    // … and so on for the remaining five values
}

Atomic member functions accept the order as an explicit parameter; the default is always seq_cst:

cpp
std::atomic<int> x{0};

x.store(42, std::memory_order_release);
int v = x.load(std::memory_order_acquire);
int old = x.exchange(1, std::memory_order_acq_rel);

// compare_exchange takes separate success and failure orders
bool ok = x.compare_exchange_strong(
    old, 2,
    std::memory_order_release,   // applied on success
    std::memory_order_relaxed);  // applied on failure

Examples

Relaxed β€” independent counter

memory_order_relaxed is correct when you need only atomicity, not ordering. A hit counter accumulated from many threads and read once at shutdown is the canonical case:

cpp
#include <atomic>
#include <thread>
#include <vector>

std::atomic<unsigned long long> total_hits{0};  // C++11

void worker(int n) {
    for (int i = 0; i < n; ++i)
        total_hits.fetch_add(1, std::memory_order_relaxed);
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 8; ++i)
        threads.emplace_back(worker, 1'000'000);
    for (auto& t : threads) t.join();
    // One final read; seq_cst (the default) is fine here
    return static_cast<int>(total_hits.load());
}

Release / Acquire β€” producer-consumer handoff

The most pervasive pattern in lock-free code: one thread publishes data and signals readiness; another consumes it.

cpp
#include <atomic>
#include <cassert>
#include <thread>

struct Payload { int x, y; };

Payload data{};                    // non-atomic shared data
std::atomic<bool> ready{false};   // C++11

void producer() {
    data = {42, 99};
    // The release ensures the 'data' write is visible before 'ready' is seen true.
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire))
        ;
    // Synchronizes-with the release store: data is fully visible here.
    assert(data.x == 42 && data.y == 99);
}

int main() {
    std::thread t1{producer}, t2{consumer};
    t1.join(); t2.join();
}

Replacing either order with memory_order_relaxed breaks the guarantee: the compiler or CPU may reorder the data write to after the flag store, leaving the consumer reading indeterminate values.

Acquire-release β€” spinlock

cpp
#include <atomic>

class Spinlock {
    // C++11–17: ATOMIC_FLAG_INIT required; C++20: default constructor clears the flag
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
    void lock() noexcept {
        while (flag_.test_and_set(std::memory_order_acquire))
            ;
    }
    void unlock() noexcept {
        flag_.clear(std::memory_order_release);
    }
};

The acquire on test_and_set pairs with the release on clear: every thread that acquires the lock sees all writes made by the previous lock holder before they released it.

Sequential consistency β€” enforcing a global order

memory_order_seq_cst is the default for all std::atomic compound operators (++, +=, etc.) and imposes a single total order on all participating operations across all threads. It is the only ordering that prevents the "independent stores" reordering visible on weakly-ordered architectures (ARM, POWER):

cpp
#include <atomic>
#include <cassert>
#include <thread>

std::atomic<int> x{0}, y{0};  // C++11
int r1 = -1, r2 = -1;

void t1() {
    x.store(1, std::memory_order_seq_cst);
    r1 = y.load(std::memory_order_seq_cst);
}

void t2() {
    y.store(1, std::memory_order_seq_cst);
    r2 = x.load(std::memory_order_seq_cst);
}

// With seq_cst: r1 == 0 && r2 == 0 is impossible.
// Either x.store or y.store is first in the total order.

With acquire/release alone, r1 == 0 && r2 == 0 is a permitted outcome on weakly-ordered hardware β€” the two stores can appear after both loads from the perspective of the other thread.

memory_order_consume β€” avoid in practice

memory_order_consume is theoretically weaker than acquire: it orders only operations that data-depend on the loaded pointer. No major compiler implements the required dependency-tracking correctly. As of C++17, implementations are permitted β€” and expected β€” to promote consume to acquire. Avoid it in new code unless targeting an embedded platform with guaranteed dependency ordering and a compiler that explicitly supports it.

Best Practices

  • Start with seq_cst. It is easiest to reason about. Profile under realistic contention before weakening anything.
  • Use release/acquire for ownership transfer. This is the right tool whenever one thread finishes writing data and another thread needs to read it.
  • Reserve relaxed for genuinely independent values. Counters, statistics, flags that are set-only and never guard other memory. Never use it to protect access to non-atomic shared state.
  • Use acq_rel on RMW operations mid-chain. When a fetch_add or compare_exchange sits between a read and a write of associated data, it must both acquire preceding writes and release subsequent ones.
  • Validate on weakly-ordered hardware or with ThreadSanitizer. x86's strong memory model masks acquire/release bugs; ARM and POWER expose them. ThreadSanitizer catches violations portably.

Common Pitfalls

Relaxed flag guarding non-atomic data. The store being atomic does not make guarded data visible. You need at least release on the write side and acquire on the read side.

Mixing seq_cst with weaker orderings on the same variable. The total-order guarantee applies only among seq_cst operations. A memory_order_release store is not part of the seq_cst total order, even if the load on the other side uses seq_cst.

Illegal memory order on compare_exchange failure. The failure order cannot be memory_order_release or memory_order_acq_rel (failure does not write), and it cannot be stronger than the success order:

cpp
// Wrong: release on failure is undefined behavior
x.compare_exchange_strong(expected, desired,
    std::memory_order_acq_rel,
    std::memory_order_release);  // UB

// Correct
x.compare_exchange_strong(expected, desired,
    std::memory_order_acq_rel,
    std::memory_order_acquire);  // OK: failure is not stronger, not release

Assuming lock-free equals fast. On architectures that require full memory barriers for seq_cst, a single atomic increment can be 10–50Γ— slower than a non-atomic one under contention. Measure before replacing a mutex with hand-rolled atomics β€” a well-implemented mutex often wins at low thread counts.

Happens-Before and Synchronizes-With in Depth

These two relations are the formal spine of the C++ memory model. Understanding them precisely is what separates correct lock-free code from code that merely works on x86.

Sequenced-before

Within a single thread, every operation is sequenced-before the next one in source order. The compiler and CPU may reorder accesses to different locations freely β€” but only as long as the result is indistinguishable to that thread. The guarantee holds for that thread alone: nothing is said about what other threads observe.

Synchronizes-with

A synchronizes-with edge crosses thread boundaries. The central rule:

A store with memory_order_release on atomic variable x synchronizes-with a load with memory_order_acquire on the same x, provided the load reads the value written by that store (or a value written by a release store that comes later in the modification order of x).

"Synchronizes-with" is not a property of the atomic variable alone β€” it is a property of a particular store-load pair. Until the acquire load actually reads the release store's value, no edge exists. That is why a consumer spin loop is necessary: the edge only fires when the load succeeds.

Happens-before

Happens-before is derived transitively from sequenced-before and synchronizes-with:

  1. If A is sequenced-before B (same thread), then A happens-before B.
  2. If A synchronizes-with B (across threads), then A happens-before B.
  3. If A happens-before X and X happens-before B, then A happens-before B.

If A happens-before B, every write performed by A (or sequenced before A) is guaranteed to be visible to B. If two accesses to the same location have no happens-before relationship between them and at least one is a write, that is a data race β€” undefined behavior with no upper bound on consequences.

cpp
#include <atomic>
#include <string>

std::atomic<int> flag{0};  // C++11
std::string payload;

void writer() {
    payload = "hello world";                     // (A) sequenced-before (B)
    flag.store(1, std::memory_order_release);    // (B) release-store
}

void reader() {
    while (flag.load(std::memory_order_acquire) == 0) {}  // (C) acquire-load
    // (B) synchronizes-with (C) because (C) read the value (B) stored.
    // By transitivity: (A) happens-before (C).
    // Therefore payload is fully visible here.
    assert(payload == "hello world");  // guaranteed β€” no UB
}

The acquire load on a different atomic, or a load that reads a value written by a relaxed store, establishes no synchronizes-with edge β€” even if the load itself uses acquire. The pairing is strict: release store on x pairs only with an acquire load on x that reads that value.

memory_order_relaxed: Atomicity Without Ordering

memory_order_relaxed guarantees only that the operation is indivisible β€” no other thread observes a torn read or a partially-applied write. It imposes no ordering constraints on surrounding memory accesses. The compiler and CPU remain free to reorder the atomic operation relative to all other accesses.

This is useful precisely when ordering is already provided by a different mechanism β€” most often a thread join.

Use case: statistics counters

When threads accumulate independent values and the final totals are only read after all threads finish, relaxed is correct. The join call itself establishes a happens-before edge that makes all writes visible:

cpp
#include <atomic>
#include <thread>
#include <vector>

std::atomic<uint64_t> requests{0};  // C++11
std::atomic<uint64_t> errors{0};

void worker(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        requests.fetch_add(1, std::memory_order_relaxed);
        if (i % 137 == 0)
            errors.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::vector<std::thread> pool;
    for (int i = 0; i < 8; ++i)
        pool.emplace_back(worker, 1'000'000);
    for (auto& t : pool)
        t.join();  // join establishes happens-before β€” all relaxed writes are visible here
    // No ordering required: final values are all we need.
    return static_cast<int>(requests.load(std::memory_order_relaxed));
}

Use case: shared_ptr reference count increment

std::shared_ptr copies increment the strong reference count and require no synchronization with other memory: the increment itself only needs to be atomic (no torn count), and no other data is being published at that moment. That is why most implementations use memory_order_relaxed for the increment half and a stronger order for the decrement that might destroy the object.

cpp
// Conceptual sketch of what shared_ptr copy does internally:
//   strong_count.fetch_add(1, std::memory_order_relaxed);
//
// The decrement on destruction needs release/acquire so that the destructor
// of the managed object happens-after all other uses:
//   if (strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1)
//       destroy_object();

Never use relaxed when the atomic operation is meant to publish or observe data held in non-atomic variables. A relaxed store of a flag gives no guarantee that the data written before it is visible to a reader β€” even if the reader loads the flag with acquire.

memory_order_acquire / memory_order_release: Directed Synchronization

Acquire and release form a directed pair. The release store acts like a one-way gate on the writing thread: no load or store sequenced before it may be reordered to appear after it (from the perspective of another thread). The acquire load acts as the matching gate on the reading thread: no load or store sequenced after it may be reordered to appear before it.

Together they implement the classic message-passing protocol:

cpp
#include <atomic>
#include <cassert>

struct Payload {
    double coefficient;
    int    sample_count;
    bool   valid;
};

Payload result{};
std::atomic<bool> ready{false};  // C++11

void producer() {
    // Step 1: Write the payload β€” sequenced-before the release store.
    result = {3.14159, 1000, true};
    // Step 2: Release store β€” acts as a "publish" fence.
    // Everything above this point is visible to any thread that
    // acquire-loads 'ready' and observes true.
    ready.store(true, std::memory_order_release);
}

void consumer() {
    // Spin until the release store is observed.
    while (!ready.load(std::memory_order_acquire))
        ;
    // Synchronizes-with the release store above.
    // result is fully visible: the entire Payload is valid.
    assert(result.coefficient > 3.0);
    assert(result.sample_count == 1000);
    assert(result.valid);
}

Acquire/release cost on real hardware

On x86 (TSO), acquire-loads compile to plain MOV instructions and release-stores to plain MOV as well β€” the hardware's strong ordering model provides the guarantee for free. On ARMv8, they map to LDAR (load-acquire) and STLR (store-release) β€” one-way barrier instructions that are cheaper than the full DMB barriers required by seq_cst. On POWER, they use lightweight lwsync. In all cases, acquire/release is measurably cheaper than seq_cst on any architecture other than x86.

Acquire/release cannot prevent IRIW

The acquire/release pair creates a directed edge β€” only the releasing thread's writes before the store are visible to the acquiring thread after the load. Two threads each doing a release store on different atomics, and two other threads each doing acquire loads on both of those atomics, are not all connected in a total order. The IRIW scenario (covered in the seq_cst section below) shows the outcome.

memory_order_acq_rel: Read-Modify-Write Operations

memory_order_acq_rel combines both semantics in one operation. It applies only to read-modify-write (RMW) operations: fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor, exchange, and compare_exchange_*. A plain load or store cannot use acq_rel.

An RMW with acq_rel:

  • Acts as an acquire with respect to everything that happens before it (it can see all writes that were released before any prior write to this atomic).
  • Acts as a release with respect to everything sequenced before it (subsequent acquire-loads of this atomic's value will see all writes preceding this operation).

Reference count decrement

The most important real-world use of acq_rel is the reference count decrement in a shared ownership scheme:

cpp
#include <atomic>
#include <cstddef>

// Stripped-down intrusive reference count β€” illustrates the pattern.
class RefCounted {
    std::atomic<std::size_t> count_{1};  // C++11
protected:
    virtual ~RefCounted() = default;
public:
    void add_ref() noexcept {
        // Increment: relaxed is sufficient.
        // No data is being published here; we only need atomicity.
        count_.fetch_add(1, std::memory_order_relaxed);
    }

    void release() noexcept {
        // Decrement: acq_rel is required.
        // 'release' semantics ensure all reads/writes to the owned data
        // sequenced before this call are visible before destruction.
        // 'acquire' semantics ensure the destructor sees all prior
        // mutations from *other* threads that also called release().
        if (count_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            // Last owner: the acq_rel on this operation synchronized-with
            // every prior release(), so destruction is safe.
            delete this;
        }
    }
};

If the decrement used only memory_order_release, the thread that hits zero could miss mutations made by other threads before their own decrements. The acquire half of acq_rel establishes happens-before with all prior release stores to the same counter β€” ensuring the destructor runs after all prior accesses to the owned object.

fetch_add mid-chain

When a fetch_add sits in a chain where the returned value is used to both observe preceding writes and publish subsequent ones, acq_rel is again correct:

cpp
std::atomic<int> token{0};

// Thread grabs the next token and publishes its own data.
// The acq_rel ensures it sees data from whoever held the previous token
// AND publishes its own data for whoever acquires the next one.
int my_token = token.fetch_add(1, std::memory_order_acq_rel);

memory_order_seq_cst: Total Global Order

memory_order_seq_cst is the default for every atomic operation and the strongest guarantee in the memory model. All seq_cst operations across all threads participate in a single, consistent total order β€” every thread agrees on the sequence in which they occurred.

The IRIW litmus test

The scenario that separates seq_cst from acquire/release is called Independent Reads of Independent Writes (IRIW). Four threads, two atomic variables:

cpp
#include <atomic>
#include <thread>
#include <cassert>

std::atomic<int> x{0}, y{0};  // C++11
int r1, r2, r3, r4;

// Thread A: stores x
void thread_a() { x.store(1, std::memory_order_seq_cst); }

// Thread B: stores y
void thread_b() { y.store(1, std::memory_order_seq_cst); }

// Thread C: loads x then y
void thread_c() {
    r1 = x.load(std::memory_order_seq_cst);
    r2 = y.load(std::memory_order_seq_cst);
}

// Thread D: loads y then x
void thread_d() {
    r3 = y.load(std::memory_order_seq_cst);
    r4 = x.load(std::memory_order_seq_cst);
}

// After all four threads complete:
// r1 == 1 && r2 == 0 && r3 == 1 && r4 == 0  is IMPOSSIBLE with seq_cst.
// Thread C and Thread D must agree on which store happened first.
// With only acquire/release: this outcome IS permitted on ARM and POWER.

With acquire/release only, thread C could see x = 1, y = 0 while thread D simultaneously sees y = 1, x = 0 β€” the two threads disagree on the relative order of the independent stores. On ARM and POWER hardware without seq_cst barriers, this is a real hardware outcome, not a hypothetical. seq_cst prevents it by inserting full memory barriers that force agreement.

Performance cost of seq_cst

The total-order guarantee requires synchronizing the store buffers of all cores. On different architectures:

  • x86 (TSO): seq_cst stores emit a MFENCE instruction (or an XCHG with memory, which implies a full fence). seq_cst loads are plain MOV. The cost is on the store side β€” typically 10–40 ns under contention.
  • ARMv8: Both loads and stores need barriers: STLR + DMB ISH for stores, LDAR for loads (ARMv8.3+ can use LDAPR for slightly cheaper acquire). Full DMB barriers are expensive on out-of-order ARM cores.
  • POWER: Uses sync (heavyweight sync instruction) for seq_cst stores, making them significantly more expensive than lwsync-based release stores.

In microbenchmarks on ARM and POWER, switching a hot path from seq_cst to acquire/release can reduce latency by 2–5x. On x86 the difference is smaller but not zero. The standard advice is: use seq_cst while developing and proving correctness, then profile under realistic multi-core load before weakening.

std::atomic_thread_fence and std::atomic_signal_fence

Fences decouple ordering from atomic operations. Instead of attaching an order to a specific atomic load or store, a fence imposes ordering on a range of surrounding operations.

atomic_thread_fence

std::atomic_thread_fence(order) emits a memory barrier that constrains how the compiler and hardware may reorder the atomic and non-atomic accesses around it, without being tied to any particular atomic variable:

cpp
#include <atomic>

std::atomic<int> flag{0};  // C++11
int data_a = 0;
int data_b = 0;

void writer() {
    data_a = 42;
    data_b = 99;
    // Release fence: no writes above may be reordered below this point.
    std::atomic_thread_fence(std::memory_order_release);
    flag.store(1, std::memory_order_relaxed);  // signal with relaxed store
}

void reader() {
    while (flag.load(std::memory_order_relaxed) == 0)
        ;
    // Acquire fence: no reads below may be reordered above this point.
    std::atomic_thread_fence(std::memory_order_acquire);
    // data_a and data_b are visible here β€” the fence pair synchronizes.
    assert(data_a == 42 && data_b == 99);
}

A release fence followed by a relaxed store, paired with a relaxed load followed by an acquire fence, is equivalent in synchronization strength to a release store paired with an acquire load. The standard requires: a release fence that is sequenced-before a relaxed atomic store, and an acquire fence that is sequenced-after a relaxed atomic load that reads the stored value, together establish synchronizes-with.

Fences are preferable when you have multiple atomic stores to publish at once and you want a single barrier rather than a release on each individual store. They also appear in implementations of lock primitives where the fence must span several operations.

atomic_signal_fence

std::atomic_signal_fence(order) is a compiler-only barrier. It generates no CPU instruction. Its sole purpose is to prevent the compiler from reordering memory accesses across the fence β€” it does not synchronize with other threads, only with signal handlers running on the same thread:

cpp
#include <atomic>
#include <csignal>

volatile std::sig_atomic_t signal_count = 0;  // POSIX signal-safe type
std::atomic<bool> data_ready{false};

void signal_handler(int) {
    ++signal_count;
    // Compiler barrier: ensures signal_count increment is not reordered
    // relative to the store below, within the same thread's instruction stream.
    std::atomic_signal_fence(std::memory_order_seq_cst);
    data_ready.store(true, std::memory_order_relaxed);
}

Use atomic_signal_fence only in signal handlers and only to prevent compiler reordering; for cross-thread synchronization always use atomic_thread_fence or per-operation memory orders.

See Also