Building a lock‑free integer counter with C11 _Atomic
Learn how to declare, initialize, and use a C11 _Atomic integer as a lock‑free counter, with compile commands, verification steps, and fallback options.
04 Dec 2025, 06:07 UTC

Desired outcome
Create a shared integer that can be safely incremented by any number of threads without using a mutex, relying on hardware‑supported atomic operations provided by the C11 _Atomic qualifier (or <stdatomic.h> types).
Prerequisites
- A compiler that implements C11 atomics (GCC 4.9+, Clang 3.1+, MSVC 2015+).
- Target architecture that supplies lock‑free atomic integer instructions (most x86‑64, ARMv8, etc.).
- Basic knowledge of creating and joining POSIX threads (
pthread). - Access to a terminal with normal user privileges; no special permissions are required to compile or run the test program.
Focused procedure
- Declare the atomic counter.
#include <stdatomic.h> #include <pthread.h> static atomic_int counter = ATOMIC_VAR_INIT(0);The variable
counteris of typeatomic_int, which guarantees that reads and writes are indivisible. - Initialize (if not using
ATOMIC_VAR_INIT).For dynamic allocation you can use:
atomic_int *counter = malloc(sizeof *counter); if (counter) atomic_init(counter, 0); - Define the increment operation.
Use
atomic_fetch_add_explicitwith a memory order that matches your needs. For a simple counter where only the final value matters,memory_order_relaxed is sufficient:static void *worker(void *arg) { int iterations = *(int *)arg; for (int i = 0; i < iterations; ++i) atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); return NULL; } - Start threads and join them.
#define N_THREADS 8 #define INCREMENTS_PER_THREAD 1000000 int main(void) { pthread_t threads[N_THREADS]; int arg = INCREMENTS_PER_THREAD; for (int t = 0; t < N_THREADS; ++t) pthread_create(&threads[t], NULL, worker, &arg); for (int t = 0; t < N_THREADS; ++t) pthread_join(threads[t], NULL); int final = atomic_load_explicit(&counter, memory_order_acquire); printf("Final counter value: %d\n", final); return 0; } - Compile the program.
Run the following command in your project directory:
gcc -std=c11 -O2 -Wall counter.c -lpthread -o counterReplace
gccwithclangor your preferred compiler if needed. - Execute and verify correctness.
./counterExpected output (with the constants above):
Final counter value: 8000000If the printed value equals
N_THREADS × INCREMENTS_PER_THREAD, the counter behaved correctly.
Expected checks
- Lock‑free status. At runtime you can query:
printf("counter is lock‑free: %d\n", atomic_is_lock_free(&counter));A return value of
2means the operation is always lock‑free on this platform. - Assembly inspection. To confirm the compiler emitted a single atomic instruction:
objdump -S counter | grep -A2 ":"Look for something like
lock xaddon x86‑64, which indicates a hardware fetch‑add. - ThreadSanitizer. Build with
-fsanitize=threadand run; no data‑race warnings should appear for the atomic counter.gcc -std=c11 -O2 -fsanitize=thread counter.c -lpthread -o counter_tsan ./counter_tsan
Limitations and recovery options
- If
atomic_is_lock_free(&counter)returns0, the implementation falls back to a mutex internally, eliminating the lock‑free advantage. In that case you can:- Use a explicit
pthread_mutex_taround a plain integer (still correct, just slower). - Fall back to GCC built‑ins like
__sync_fetch_and_addif you need a portable lock‑free primitive on older toolchains.
- Use a explicit
- Using
memory_order_relaxedfor increments means other threads may observe stale values if they rely on the counter for ordering. When ordering matters (e.g., a flag that signals completion), replace the increment withmemory_order_releaseand the read withmemory_order_acquire, or usememory_order_seq_cstfor simplest reasoning. - The counter only guarantees atomicity for a single variable. Multi‑variable invariants still require additional synchronization (mutexes, condition variables, etc.).
Practical way to check the result
After running the stress test, compare the final value with the expected product. A simple script can automate the check:
#!/bin/bash
EXPECTED=$((N_THREADS * INCREMENTS_PER_THREAD))
ACTUAL=$(./counter | grep -o '[0-9]\+')
if [ "$ACTUAL" -eq "$EXPECTED" ]; then
echo "PASS"
else
echo "FAIL: expected $EXPECTED, got $ACTUAL"
exit 1
fi
Run the script; a non‑zero exit indicates a problem that warrants reviewing compiler flags, memory order, or platform support.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.