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 DAcquire-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 DMemory Order Summary
| Order | Guarantee | Use Case |
|---|---|---|
| seq_cst | Total global order | Default, when in doubt |
| acquire | See all writes before release | Load synchronization |
| release | Make prior writes visible | Store synchronization |
| acq_rel | Both acquire and release | Read-modify-write ops |
| relaxed | No ordering guarantees | Counters, 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