Frontend Labs
⋯

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.

Generated from real-life-c-guide.md · 8 Oct 2026 16:36