C11 Atomics and Memory Order
C11 Atomics and Memory Order
Thesis
_Atomic variables and memory_order_* are how C11 expresses shared-memory concurrency without guessing what the compiler and CPU will reorder. A plain int counter with two threads is a data race; atomic_fetch_add is not. This chapter builds a tiny two-thread counter and names the memory orders you actually need.
Mental model
atomic_int counter;
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
│ │
│ └─ how visible to other threads
└─ RMW that cannot tear
memory_order_relaxed: atomicity only; no ordering with other memory.memory_order_acquire/release: pair a producer store with a consumer load.memory_order_seq_cst: default when you call the non-_explicitAPIs; strongest and usually fine until a profile says otherwise.
Worked example
Verified with GCC 14.2, -std=c11 -pthread.
/* atomics.c */
#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>
atomic_int counter = 0;
int worker(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++)
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
return 0;
}
int main(void) {
thrd_t t1, t2;
thrd_create(&t1, worker, NULL);
thrd_create(&t2, worker, NULL);
thrd_join(t1, NULL);
thrd_join(t2, NULL);
printf("counter=%d\n", atomic_load(&counter));
printf("memory_order_seq_cst=%d relaxed=%d\n",
(int)memory_order_seq_cst, (int)memory_order_relaxed);
return 0;
}gcc -O2 -std=c11 atomics.c -pthread -o atomics && ./atomicsObserved on this box:
counter=200000
memory_order_seq_cst=5 relaxed=0
Two hundred thousand increments land exactly — no lost updates. The enum values are implementation-defined integers; do not hard-code them.
Trap
Mixing plain loads/stores of an _Atomic with non-atomic access to the same object. Once a variable is _Atomic, every access goes through the atomic API (or _Atomic-qualified lvalues). Also: volatile is not a substitute for atomics.
Boring rule
Start with the non-_explicit operations (atomic_fetch_add, atomic_load, atomic_store) — they are sequentially consistent. Reach for relaxed only when a shared counter truly needs no ordering, and document why.
Try this
- Drop
_Atomicand use a plainint; run under ThreadSanitizer (-fsanitize=thread) and read the race report. - Replace
relaxedwithseq_cstand confirm the count is unchanged (correctness) while thinking about fence cost on weakly ordered CPUs. - Build a release/acquire hand-off: thread A stores a payload then
atomic_store_explicit(&ready, 1, memory_order_release); thread B spins withacquire.
Sources
- ISO/IEC 9899:2011 §7.17 (
stdatomic.h) - GCC documentation: Using the GNU Compiler Collection — Atomics
- C11 draft N1570 (public reference for the memory model)