Memory Model

Understanding memory ordering and atomics

Sequential Consistency

The default memory order provides the strongest guarantees but may be slower.

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

// Thread 1
data = 42;                          // A
ready.store(true);                  // B (seq_cst by default)

// Thread 2
while (!ready.load()) {}            // C (seq_cst by default)
assert(data == 42);                 // D - always passes!

// Sequential consistency guarantees:
// All threads see operations in the same global order
// B happens-before C, so A happens-before D

Acquire-Release

Pair synchronization: release makes prior writes visible to acquire.

std::atomic<int*> ptr{nullptr};
int data = 0;

// Thread 1: Producer
data = 42;  // A
ptr.store(&data, std::memory_order_release);  // B

// Thread 2: Consumer
int* p = ptr.load(std::memory_order_acquire);  // C
if (p != nullptr) {
    assert(*p == 42);  // D - guaranteed!
}

// Release in B synchronizes-with acquire in C
// This creates a happens-before relationship:
// A happens-before B, B synchronizes-with C, C happens-before D

Memory Order Summary

OrderGuaranteeUse Case
seq_cstTotal global orderDefault, when in doubt
acquireSee all writes before releaseLoad synchronization
releaseMake prior writes visibleStore synchronization
acq_relBoth acquire and releaseRead-modify-write ops
relaxedNo ordering guaranteesCounters, when order doesn't matter

Common Patterns

// Spinlock with acquire-release
class Spinlock {
    std::atomic<bool> locked{false};
public:
    void lock() {
        while (locked.exchange(true, std::memory_order_acquire)) {
            // Spin until we acquire the lock
        }
    }
    
    void unlock() {
        locked.store(false, std::memory_order_release);
    }
};

// Simple counter - relaxed is fine
std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);
// Only care about atomicity, not ordering