Skip to content

Memory Order — Memory Ordering, Atomics, Fence Semantics

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Memory Order. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ atomic operations with memory ordering parameters establish inter-thread synchronization, from sequentially consistent ordering to relaxed semantics, providing the foundation for lock-free data structures.

What You'll Learn

You will use std::atomic<T> for lock-free operations on shared data, understand the six memory ordering modes (memory_order_seq_cst, acquire, release, acq_rel, consume, relaxed), apply acquire-release semantics for correct synchronization, use atomic fences for standalone ordering, and avoid common pitfalls like data races and torn reads.

Why It Matters

Modern CPUs reorder memory operations for performance. Without correct memory ordering, concurrent code can see stale values, read partial updates, or observe operations in inconsistent orders. C++ atomics with memory ordering give you portable control over synchronization that maps to the correct CPU instructions on every architecture (x86, ARM, RISC-V).

Learning Path

graph LR
    A["27: Object Lifetimes"] --> B["28: Memory Order"]
    B --> C["29: STL Overview"]
    C --> D["30: Vector"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Atomic Basics

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

int main() {
    std::atomic<int> counter(0);
    
    std::thread t1([&]() {
        for (int i = 0; i < 10000; ++i) counter.fetch_add(1);
    });
    
    std::thread t2([&]() {
        for (int i = 0; i < 10000; ++i) counter.fetch_add(1);
    });
    
    t1.join();
    t2.join();
    
    std::cout << counter.load() << "\n";  // 20000 (guaranteed)
}

Without std::atomic<int>, the ++ operation would be three separate instructions (read, modify, write), and two threads could interleave, losing updates.

Memory Order Overview

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

std::atomic<int> data(0);
std::atomic<bool> ready(false);

void producer() {
    data.store(42, std::memory_order_release);
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {
        // spin
    }
    std::cout << data.load(std::memory_order_acquire) << "\n";  // guaranteed 42
}

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

The six memory orders, from strongest to weakest:

Order Description
memory_order_seq_cst Sequential consistency (default)
memory_order_acq_rel Acquire-release for read-modify-write
memory_order_acquire Prevents reordering after the load
memory_order_release Prevents reordering before the store
memory_order_consume Data-dependent ordering (use acquire instead)
memory_order_relaxed No ordering constraints

Sequential Consistency

#include <atomic>
#include <thread>

std::atomic<bool> x(false), y(false);
std::atomic<int> z(0);

void writeX() {
    x.store(true, std::memory_order_seq_cst);
}

void writeY() {
    y.store(true, std::memory_order_seq_cst);
}

void readXthenY() {
    while (!x.load(std::memory_order_seq_cst)) {}
    if (y.load(std::memory_order_seq_cst)) ++z;
}

void readYthenX() {
    while (!y.load(std::memory_order_seq_cst)) {}
    if (x.load(std::memory_order_seq_cst)) ++z;
}

int main() {
    std::thread t1(writeX);
    std::thread t2(writeY);
    std::thread t3(readXthenY);
    std::thread t4(readYthenX);
    
    t1.join(); t2.join(); t3.join(); t4.join();
    
    // With seq_cst, z must be 1 or 2 (never 0)
    // With relaxed, z could be 0
}

Sequential consistency provides a single total order of all sequentially-consistent operations, as if all threads executed in some interleaving. This is the default and easiest to reason about, but has the highest cost.

Acquire-Release Semantics

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

std::atomic<int> guard(0);
int payload = 0;

void producer() {
    payload = 42;                             // A
    guard.store(1, std::memory_order_release);  // B
}

void consumer() {
    while (guard.load(std::memory_order_acquire) == 0) {}  // C
    assert(payload == 42);  // guaranteed: C synchronizes with B
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
}
  • Release (in producer): all writes before the store are visible to the thread that acquires
  • Acquire (in consumer): all reads after the load see the values written before release

This establishes a happens-before relationship between the release and acquire operations.

Relaxed Ordering

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

std::atomic<int> counter(0);

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

int main() {
    std::thread threads[10];
    for (auto& t : threads) {
        t = std::thread(worker);
    }
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << counter.load() << "\n";  // 10000 (atomic, but no ordering)
}

Relaxed operations are atomic (no torn reads) but provide no synchronization or ordering. Use them for counters that do not guard other data.

Read-Modify-Write Operations

#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> value(10);
    
    // fetch_add: return old value, add
    int old = value.fetch_add(5);
    std::cout << old << " " << value.load() << "\n";  // 10 15
    
    // exchange: set new, return old
    int prev = value.exchange(20);
    std::cout << prev << " " << value.load() << "\n";  // 15 20
    
    // compare_exchange_weak: CAS loop
    int expected = 20;
    bool success = value.compare_exchange_weak(expected, 30);
    std::cout << success << " " << value.load() << "\n";  // 1 30
    
    // compare_exchange_strong: no spurious failure
    expected = 30;
    success = value.compare_exchange_strong(expected, 40);
    std::cout << success << " " << value.load() << "\n";  // 1 40
}

Atomic Fences

#include <atomic>
#include <thread>

std::atomic<bool> flag(false);
int data = 0;

void producer() {
    data = 42;
    std::atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void consumer() {
    while (!flag.load(std::memory_order_relaxed)) {}
    std::atomic_thread_fence(std::memory_order_acquire);
    assert(data == 42);  // guaranteed by fence pairing
}

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

Fences establish ordering without being tied to a specific atomic variable. They are more flexible but complex.

Performance Considerations

On x86, all stores are release stores and all loads are acquire loads (except for movnti). This means:

  • memory_order_seq_cst requires mfence or lock prefix (most expensive)
  • memory_order_acquire and memory_order_release are free on x86
  • memory_order_relaxed avoids any CPU barriers

On ARM and RISC-V, every memory order below seq_cst maps to specific barrier instructions, making the choice of ordering more impactful.

Common Mistakes

Mistake 1: Using volatile for Thread Synchronization

volatile int flag = 0;  // WRONG for threading

volatile does not provide atomicity or memory ordering. Use std::atomic<int>.

Mistake 2: Assuming relaxed Atomic is Useless

Relaxed is fine for counters, statistics, and flags that do not guard other data.

Mistake 3: Forgetting That load() and store() Default to seq_cst

flag.store(true);  // equivalent to memory_order_seq_cst (most expensive)

Specify relaxed when full ordering is not needed.

Mistake 4: Data Race on Non-Atomic Data

int shared = 0;
std::atomic<bool> ready(false);
// Thread 1: shared = 42; ready.store(true, memory_order_release);
// Thread 2: while(!ready.load(memory_order_acquire)) {}; assert(shared == 42);

This is correct! The acquire-release pair synchronizes the non-atomic shared access.

Mistake 5: ABA Problem in Lock-Free Structures

When using compare_exchange_weak, a value can change from A to B and back to A between reads. Use version counters or hazard pointers.

Mistake 6: Overusing Sequential Consistency

counter.fetch_add(1);  // default seq_cst is overkill for a counter

Use memory_order_relaxed for simple counters.

Practice Questions

  1. What is the difference between memory_order_relaxed and memory_order_seq_cst?
  2. What does acquire-release ordering guarantee?
  3. Write a lock-free thread-safe counter using fetch_add with memory_order_relaxed.
  4. When would you use a fence instead of an atomic operation with ordering?
  5. Why does x86 have relatively cheap acquire-release semantics?

Challenge

Implement a simple spinlock mutex using std::atomic<bool> with test_and_set (or exchange). Ensure correct memory ordering. Compare its performance with std::mutex in a contended scenario.

FAQ

Is `std::atomic` lock-free for all types?

No. std::atomic<T> is lock-free if std::atomic<T>::is_always_lock_free is true. For larger types, it may use a mutex internally. You can check with is_lock_free().

What is the ABA problem?

In CAS operations, a value changes from A to B and back to A between the read and the CAS, causing the CAS to succeed when the state has actually changed. Solved with version counters or tagged pointers.

What is the difference between `memory_order_consume` and `memory_order_acquire`?

consume provides ordering only for data-dependent operations. Current compilers promote consume to acquire due to implementation difficulties. Do not use consume.

How do atomics work on ARM vs x86?

x86 provides strong ordering (acquire/release are free). ARM uses explicit barriers (dmb instructions). seq_cst is expensive on both: mfence on x86, dmb sy on ARM.

Can I use `std::atomic` with custom types?

Yes, but the type must be trivially copyable. Large atomic types may use internal locks, defeating the purpose of lock-free operations.

What is a 'data race'?

Two or more threads concurrently access the same memory location, at least one access is a write, and at least one access is not atomic. Data races are undefined behavior.

Mini Project

Build a lock-free ring buffer for single producer, single consumer:

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

template <typename T, size_t Capacity>
class SPSCRingBuffer {
private:
    static constexpr size_t Mask = Capacity - 1;
    T buffer_[Capacity];
    std::atomic<size_t> head_{0};
    std::atomic<size_t> tail_{0};
    
public:
    bool push(const T& value) {
        size_t tail = tail_.load(std::memory_order_relaxed);
        size_t next = (tail + 1) & Mask;
        if (next == head_.load(std::memory_order_acquire)) {
            return false;  // full
        }
        buffer_[tail] = value;
        tail_.store(next, std::memory_order_release);
        return true;
    }
    
    bool pop(T& value) {
        size_t head = head_.load(std::memory_order_relaxed);
        if (head == tail_.load(std::memory_order_acquire)) {
            return false;  // empty
        }
        value = buffer_[head];
        head_.store((head + 1) & Mask, std::memory_order_release);
        return true;
    }
};

int main() {
    SPSCRingBuffer<int, 256> buffer;
    constexpr int NumItems = 10000;
    
    std::thread producer([&]() {
        for (int i = 0; i < NumItems; ++i) {
            while (!buffer.push(i)) {}
        }
    });
    
    std::thread consumer([&]() {
        std::vector<int> received;
        for (int i = 0; i < NumItems; ++i) {
            int value;
            while (!buffer.pop(value)) {}
            received.push_back(value);
        }
        std::cout << "Received " << received.size() << " items\n";
    });
    
    producer.join();
    consumer.join();
}

What's Next

Memory ordering is the foundation of lock-free programming. The next lesson begins Module 4 on STL Containers, starting with an overview of containers, iterators, and algorithm complexity.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro