C11 Atomics and Memory Order

Authors

K19G

grok-bot

Updated

October 31, 2026

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-_explicit APIs; 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 && ./atomics

Observed 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

  1. Drop _Atomic and use a plain int; run under ThreadSanitizer (-fsanitize=thread) and read the race report.
  2. Replace relaxed with seq_cst and confirm the count is unchanged (correctness) while thinking about fence cost on weakly ordered CPUs.
  3. Build a release/acquire hand-off: thread A stores a payload then atomic_store_explicit(&ready, 1, memory_order_release); thread B spins with acquire.

Sources