Part VII — Expert: internals, allocators, API design
38. The C11 memory model: atomics, ordering, lock-free basics
Section 20.4 used atomic_fetch_add with its default ordering
(memory_order_seq_cst — sequentially consistent, the easiest to reason
about, the most expensive on weakly-ordered hardware like ARM). Real
lock-free code often relaxes this deliberately for performance:
#include <stdatomic.h>
atomic_int ready = 0;
int payload = 0; // NOT atomic — protected by the ordering below, not by its own type
void producer(void) {
payload = 42; // 1. write the data
atomic_store_explicit(&ready, 1, memory_order_release); // 2. THEN publish "it's ready"
}
int consumer(void) {
while (!atomic_load_explicit(&ready, memory_order_acquire)) { } // spin until published
return payload; // guaranteed to see 42 — the acquire/release pair forms a "happens-before" edge
}
memory_order_release on the store and memory_order_acquire on the
load form a synchronization point: everything the producer wrote
before the release-store (here, payload = 42) is guaranteed visible
to the consumer after its acquire-load sees the new value. Without this
pairing (e.g., using memory_order_relaxed on both), the compiler and
CPU are both free to reorder the two writes, and the consumer could
observe ready == 1 while still reading payload's old value — a bug
that's nearly impossible to reproduce in testing and shows up only under
production load on weakly-ordered hardware.
| Ordering | Cost | Use for |
|---|---|---|
memory_order_relaxed |
Cheapest | Independent counters/stats where ordering relative to other data doesn't matter |
memory_order_acquire/_release |
Moderate | Publishing a pointer/flag after finishing writes to the data it guards (the pattern above) |
memory_order_seq_cst |
Most expensive | Default/safe choice; use until you've measured a real bottleneck |
Real-world example. This exact acquire/release handshake is how lock-free single-producer/single-consumer ring buffers (used in audio-processing callbacks, kernel-bypass networking, and Redis-adjacent high-throughput queues) hand data between threads without ever taking a mutex — the mutex-based producer/consumer from Section 20.3, with the lock replaced by two atomics and a lot more care.