C11 Atomics: Lock-Free Shared State and Memory Ordering
C11 <stdatomic.h> provides standard atomic types and operations for thread-safe shared data. Learn how to use atomic counters and publication flags, choose correct memory orders, and verify lock-free support.
14 Oct 2026, 21:59 UTC

When two threads touch the same variable without synchronization, the C standard defines the behavior as undefined—not merely unpredictable. A plain int incremented by multiple threads can lose updates, and the compiler may assume no other thread modifies it, optimizing away repeated reads. C11 introduces <stdatomic.h> to make individual accesses to shared objects well-defined without requiring a mutex on every operation.
The practical takeaway: declare shared variables with the _Atomic qualifier or the atomic_* typedefs, use the explicit atomic_*_explicit functions to state the ordering you need, and never assume the result is lock-free until you have checked.
How C11 atomics work
An atomic type tells the compiler that every read and write to that object must be indivisible with respect to other threads. You can declare one in two equivalent ways:
_Atomic int counter = 0;
atomic_int counter = 0; /* typedef from <stdatomic.h> */
Operations such as atomic_load, atomic_store, atomic_fetch_add, and atomic_compare_exchange_strong are guaranteed to be atomic. On x86_64 or modern ARM, the compiler often emits a hardware instruction such as LOCK XADD or LDADD. On targets without suitable instructions, the implementation falls back to a lock, so the operation remains correct but may block.
A worked example: independent counter and published data
Consider two common patterns. First, several threads increment a shared total; we only need the additions to be indivisible, so a relaxed ordering is sufficient. Second, one thread prepares a value and publishes it behind a flag; here we need ordering so the reader cannot see the flag before the value.
#include <stdatomic.h>
#include <stdbool.h>
#include <threads.h>
atomic_int total = 0; /* shared accumulator */
atomic_bool data_ready = 0; /* publication flag */
int shared_value = 0; /* plain payload */
int adder(void *arg) {
for (int i = 0; i < 1000; i++) {
/* Only atomicity matters; no ordering constraint */
atomic_fetch_add_explicit(&total, 1, memory_order_relaxed);
}
return 0;
}
int publisher(void *arg) {
shared_value = 42;
/* Release: prior writes cannot move after this store */
atomic_store_explicit(&data_ready, true, memory_order_release);
return 0;
}
int reader(void *arg) {
/* Acquire: later reads cannot move before this load */
while (!atomic_load_explicit(&data_ready, memory_order_acquire)) {
/* spin */
}
/* shared_value is safely visible here */
return 0;
}
The counter uses memory_order_relaxed because no thread depends on the order of the increments—only the final sum matters. The flag uses memory_order_release and memory_order_acquire to create a happens-before edge: everything the publisher wrote before the release store is visible to the reader after the matching acquire load.
Choosing a memory order
C11 defines five ordering constraints. The default for the non-explicit functions is memory_order_seq_cst, the strongest and usually the safest choice.
| Order | Practical use |
|---|---|
memory_order_relaxed | Counters or statistics where only atomicity matters. |
memory_order_release | Publishing data before setting a flag. |
memory_order_acquire | Reading data after observing a flag. |
memory_order_acq_rel | Read-modify-write operations that both consume and publish. |
memory_order_seq_cst | Default; use when you need a single total order across threads. |
Using memory_order_relaxed on a flag that guards other data is a common source of subtle bugs. The compiler or CPU may reorder the flag store before the data store, so a reader observes the flag but stale data.
Limits and common mistakes
Lock-free is not guaranteed
The C standard only guarantees atomicity, not that operations are lock-free. A type is lock-free only if the compiler and target architecture support it. You can query this at runtime with the standard macro:
#include <stdatomic.h>
/* ... */
if (atomic_is_lock_free(&total)) {
/* hardware path */
} else {
/* lock-based fallback */
}
GCC and Clang also expose the builtin __atomic_is_lock_free(sizeof(total), &total). On small embedded cores, even atomic_int may use a lock, which can invert your performance assumptions.
Atomic pointers are architecture-dependent
Comparing or swapping pointers atomically requires declaring them as _Atomic(void *) or _Atomic(T *). Not every architecture provides lock-free pointer-sized atomics, and some older cores lack the instructions entirely.
Atomics do not protect invariants
Atomic operations make a single access indivisible; they do not make a sequence of accesses atomic. This check-then-act pattern is still racy:
if (atomic_load(&total) > 0) {
atomic_store(&total, atomic_load(&total) - 1); /* race window */
}
Use atomic_fetch_sub for the decrement, or a compare-and-swap loop when the update depends on the current value. If the invariant spans multiple variables, a mutex is usually clearer and safer.
Verifying behavior
Compile a small program that exercises the atomic paths and run it under ThreadSanitizer or Valgrind's Helgrind. For example, from your shell or build system:
gcc -std=c11 -pthread -fsanitize=thread -o atomic_demo atomic_demo.c
./atomic_demo
These tools understand C11 atomics and will report unsynchronized accesses to non-atomic variables. Because lock-free status varies by architecture, test on the actual target hardware rather than assuming behavior from a development workstation.
When to reach for a mutex instead
Atomics excel at simple shared state: counters, flags, and single-pointer publications. When you need to update several fields together, maintain a queue, or protect a complex invariant, a mutex and condition variables remain the more maintainable choice. The standard library atomics are a precise tool, not a general replacement for locking.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.