Home › Concurrency › Memory model
guide low

Memory ordering

Problem Compilers and CPUs reorder memory operations, so lock-free code, flags and custom locks can break in ways one thread never sees. This guide answers what ordering guarantee each atomic operation needs.

What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.

atomicsmemory-modelacquire-releaselock-freec11
Overview

Compilers and CPUs reorder memory operations as long as a single thread cannot tell. An atomic's ordering says what other threads may observe. Mental model: a Release store publishes every earlier write; an Acquire load that reads it receives them all. Relaxed is atomic but orders nothing. SeqCst adds one global order, needed only when a thread stores one location and then loads another (Dekker pattern).

Key facts

Use when

  • Writing lock-free structures, flags, refcounts, one-shot publication, custom locks.
  • OS kernels: the Linux kernel documents its own barrier rules and memory model (LKMM, memory barriers).
  • Language runtimes and standard libraries: reference counts as in Rust's Arc (decrement Release, then an Acquire fence before free).
  • Networking stacks: DPDK's rte_ring publishes ring tails with C11 acquire and release orderings (rte_ring_c11_pvt.h).

Avoid when

  • A Mutex, channel, OnceLock or Arc already does the job.

Deep dive

Decision table

SituationOrderingWhy
Event/statistics counter, read approximatelyfetch_add(Relaxed)Nothing else is published through it
Publish data via a flag or pointerstore Release, load AcquireMessage passing: writes before the store are visible after the load
Lock acquire / unlockCAS success Acquire, unlock store ReleaseCritical-section contents stay inside
Refcount incrementRelaxedYou already hold a reference
Refcount decrementfetch_sub(Release); if it hit 0: fence(Acquire) then freeAll other owners' uses happen-before the free
Store my flag, then read yours (Dekker, sleep/wake flags)SeqCst on both sides, or fence(SeqCst) between store and loadOnly SeqCst forbids store→load reordering
CAS retry loop that publishescompare_exchange_weak(.., AcqRel or Release, Relaxed)Failure path publishes nothing
Lazy one-time initOnceLock / std::call_onceHandles the races and blocking for you
Reading many relaxed atomics, then ordering oncerelaxed loads + one fence(Acquire)Fence upgrades all prior loads
Pointer to immutable data (RCU style)Acquire loadconsume is unimplemented in practice; compilers promote it to acquire
Operationx86-64AArch64 (ARMv8.1+ LSE)
load Relaxed / Acquire / SeqCstmov / mov / movldr / ldar (or ldapr) / ldar
store Relaxed / Release / SeqCstmov / mov / xchgstr / stlr / stlr
fetch_add any orderinglock xaddldadd (+a/l/al suffix)
fence(Acquire/Release)compiler barrier onlydmb ishld / dmb ish
fence(SeqCst)mfence (or a locked op)dmb ish

Code

use std::cell::UnsafeCell;
use std::sync::atomic::{fence, AtomicBool, AtomicUsize, Ordering::*};

// 1. Message passing (one-shot). Release/Acquire make the plain write to `data` visible.
pub struct Mailbox { ready: AtomicBool, data: UnsafeCell<u64> }
unsafe impl Sync for Mailbox {}
impl Mailbox {
    pub fn send(&self, v: u64) { // call at most once
        unsafe { *self.data.get() = v; }
        self.ready.store(true, Release);
    }
    pub fn recv(&self) -> Option<u64> {
        if self.ready.load(Acquire) { Some(unsafe { *self.data.get() }) } else { None }
    }
}

// 2. Reference count, as in std::sync::Arc.
pub fn rc_inc(rc: &AtomicUsize) { rc.fetch_add(1, Relaxed); }
pub fn rc_dec(rc: &AtomicUsize) -> bool { // true => caller frees
    if rc.fetch_sub(1, Release) != 1 { return false; }
    fence(Acquire); // synchronize with every other owner's Release decrement
    true
}

// 3. Store buffering: "a == 0 && b == 0" is impossible only with SeqCst.
pub static X: AtomicBool = AtomicBool::new(false);
pub static Y: AtomicBool = AtomicBool::new(false);
pub fn thread_a() -> bool { X.store(true, SeqCst); Y.load(SeqCst) }
pub fn thread_b() -> bool { Y.store(true, SeqCst); X.load(SeqCst) }
// With Release/Acquire both can return false: each store sits in its core's store buffer.

// 4. Spin on a relaxed load, then one acquire fence (cheaper loop body on ARM).
pub fn wait_ready(flag: &AtomicBool) {
    while !flag.load(Relaxed) { std::hint::spin_loop(); }
    fence(Acquire);
}

Efficiency

  • Cost: on x86-64 acquire loads and release stores are plain mov; a SeqCst store is an xchg, order of 20 cycles. On AArch64 they are ldar/stlr, relaxed is ldr/str. Contention costs far more than ordering: order of 50-100+ ns per line transfer. Bound: coherence traffic, not the fence.
  • Levers:
    • Relaxed for counters that publish nothing — plain lock xadd or ldadd, no fences — statistics, refcount increments
    • Release/Acquire instead of SeqCst — plain mov on x86, no xchg or mfence (table above) — message passing, locks
    • relaxed spin loop plus one fence(Acquire) — cheaper loop body on ARM — flag waits
    • build for ARMv8.1 LSE (-march=armv8.1-a, Rust +lse) — single-instruction atomics instead of LL/SC retry loops (see Pitfalls) — Graviton, Apple Silicon
    • remove the shared write instead of weakening its order (sharded counters) — contention dominates — hot counters
  • Naive → standard → state of the art:
    • SeqCst on every access · an xchg per store, order of 20 cycles
    • Release/Acquire per pattern (decision table) · plain mov on x86
    • relaxed accesses with a single fence, LSE atomics, no shared writes on the hot path · order of 1 ns per operation

Rules that matter

  • Happens-before comes from a release operation and an acquire operation on the same atomic, where the acquire reads the value written by the release (or a later RMW in its release sequence).
  • Acquire/release on different variables gives no ordering between them.
  • RMWs (fetch_add, swap, CAS) always read the latest value in that location's modification order, even when Relaxed; plain Relaxed loads may read stale values.
  • Every atomic location has a single total modification order that all threads agree on (coherence), even with Relaxed.
  • Fences: a fence(Release) before a relaxed store acts like a release store; a fence(Acquire) after a relaxed load acts like an acquire load.
  • SeqCst operations form one total order consistent with happens-before; mixing SeqCst with weaker accesses on the same locations is subtle (C++20 repaired the rules after Lahav et al. 2017).

Pitfalls

  • "It works on x86": TSO hides missing acquire/release at the hardware level, but the compiler can still reorder. Test on ARM (Apple Silicon, Graviton) and with model checkers.
  • load then store instead of an RMW: lost updates.
  • volatile (C/C++) or read_volatile (Rust) is not atomic and orders nothing across threads.
  • Using Acquire on a store or Release on a load: Rust panics at runtime for load(Release); C++ makes it undefined.
  • Flag-based sleep/wake (set "sleeping", check queue; producer pushes, checks "sleeping") with only acquire/release: both sides can miss each other. Needs SeqCst fences or a lock.
  • Pre-LSE ARM (ARMv8.0) atomics are LL/SC loops that livelock-ish under contention; build with LSE (-march=armv8.1-a, Rust +lse, or GCC outline atomics).
  • Benchmarks of "SeqCst is slow" usually measure contention, not the fence.

Tooling

  • Rust: loom (exhaustive interleavings under the C11 model), Miri (detects data races and some weak-memory behaviours), ThreadSanitizer (-Zsanitizer=thread).
  • C/C++: ThreadSanitizer, GenMC, CDSChecker, herd7 litmus tests for hardware models.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
loomRusttokio-rs/loomcargo add loom --dev————exhaustive interleavings under the C11 model
shuttleRustawslabs/shuttlecargo add shuttle --dev————schedule-exploration testing of concurrent Rust code
MiriRustrust-lang/mirirustup +nightly component add miri————detects data races and some weak-memory behaviours
GenMCC++MPI-SWS/genmcn/a (build from source)————stateless model checker for C/C++ under weak memory
herdtools7OCamlherd/herdtools7opam install herdtools7————herd7 litmus tests for hardware and language models

Sources

  • Hans-J. Boehm, Sarita V. Adve, Foundations of the C++ Concurrency Memory Model, PLDI 2008. paper
  • Peter Sewell, Susmit Sarkar, Scott Owens, Francesco Zappa Nardelli, Magnus O. Myreen, x86-TSO: A Rigorous and Usable Programmer's Model for x86 Multiprocessors, Communications of the ACM 53(7), 2010. paper
  • Ori Lahav, Viktor Vafeiadis, Jeehoon Kang, Chung-Kil Hur, Derek Dreyer, Repairing Sequential Consistency in C/C++11, PLDI 2017. paper
  • Mara Bos, Rust Atomics and Locks, O'Reilly, 2023. book
  • Paul E. McKenney, Is Parallel Programming Hard, And, If So, What Can You Do About It?, kernel.org (continuously updated). book

Used by

Buffer pools for zero-allocation I/OA storage engine, network server or log writer doing I/O at high rates loses time and memory predictability by acquiring a fresh buffer for every read or write. It needs buffer management that costs almost nothing per operation.
cache coherence protocols · plannedMESI and MOESI, snooping vs directories: how cores keep caches consistent and what that costs
Incremental and concurrent garbage collectionA program with a large managed heap must reclaim unreachable memory while it keeps running; a collector that stops everything to trace the heap freezes requests, frames or UI for as long as the heap is big.MPMC bounded queueThread pools, log pipelines and object pools need a fixed-capacity queue that many producer and consumer threads can use at the same time without a lock.
parallel tree search · plannedSearch one game tree on many threads: shared lossy transposition table (Lazy SMP), split points (YBWC), virtual loss for MCTS
RCU (read-copy-update)Data that is read constantly and changed rarely (routing tables, configs) suffers when readers pay for locking. This makes reads nearly free while updates stay safe, and decides when old versions can be freed.SeqlockSmall records read very often and written occasionally (clocks, latest quotes, config snapshots) make readers pay for locks and cache-line traffic. This lets many cores read without disturbing a single writer.Sharded countersA statistic updated by every core, such as a request count, stops scaling because all cores fight over one memory location. This keeps hot counters fast on many-core machines.SPSC ring bufferA pipeline where exactly one thread hands data to exactly one other (network receiver to worker, audio callback, logger) needs a very fast queue without locks or atomic read-modify-write operations.
thread caching allocator · plannedGeneral-purpose malloc with size classes, per-thread caches and free-list sharding (tcmalloc, mimalloc)
Transposition tableA tree search keeps re-searching states it has already analysed, reached by different orders of steps or in earlier iterations, wasting time. The search needs a way to remember and reuse past results in bounded memory.Work-stealing dequeParallel runtimes with many short tasks bottleneck on a single shared task queue. This spreads tasks over workers and balances load when some run out of work.

Systems (6):

Alternatives and related