Table of Contents

stdatomic.h

stdatomic.h (C11) gives you atomic types and operations for lock-free concurrent programming. An atomic operation completes without interruption: no other thread can observe the object in a half-updated state. This is the C standard's portable answer to compiler-specific builtins like __sync_fetch_and_add and __atomic_compare_exchange.

If you have ever protected a shared counter with a mutex and thought “this seems like overkill for a single increment”, atomics are what you want instead. A mutex involves at least two system calls and kernel transitions; an atomic increment compiles to a single locked CPU instruction.

#include <stdatomic.h>
 
atomic_int counter = 0;
 
// from any thread:
atomic_fetch_add(&counter, 1);     // race-free increment
int v = atomic_load(&counter);     // race-free read
atomic_store(&counter, 0);         // race-free write

The standard atomic types are atomic_int, atomic_long, atomic_uint64_t, etc., or declared with _Atomic(type). The key operation for lock-free algorithms is compare-and-swap:

int expected = 5;
bool ok = atomic_compare_exchange_strong(&x, &expected, 10);
// if *x == expected: sets *x = 10, returns true
// if *x != expected: writes current *x into expected, returns false

Every atomic operation has an optional memory order. The default (memory_order_seq_cst) is the strongest and most expensive. For performance-critical code, _explicit variants let you choose:

atomic_store_explicit(&flag, 1, memory_order_release);  // publish a write
int f = atomic_load_explicit(&flag, memory_order_acquire); // see it
atomic_fetch_add_explicit(&x, 1, memory_order_relaxed);  // just atomicity, no ordering

release/acquire pairs are sufficient for most producer-consumer patterns. relaxed gives only atomicity with no synchronisation between threads.

Practice

// compile: gcc -O2 -o atomicdemo atomicdemo.c -lpthread
// run: ./atomicdemo
// description: two threads increment a shared counter 1M times each; should total 2M
 
#include <stdatomic.h>
#include <pthread.h>
#include <stdio.h>
 
atomic_int counter = 0;
 
void *worker(void *arg) {
    for (int i = 0; i < 1000000; i++)
        atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
    return NULL;
}
 
int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("counter = %d (expected 2000000)\n", atomic_load(&counter));
    return 0;
}

Run it: you will get exactly 2000000 every time. Now change atomic_fetch_add_explicit to a plain counter++ (a non-atomic read-modify-write), recompile without the atomic header, and run it a few times. You will get different results below 2000000 each time — that is the data race. The atomic version compiles to a single lock xadd instruction on x86, which is why it works without a mutex.