Table of Contents

Atomics

Atomics are operations on a shared variable that execute as a single, indivisible step, even when multiple threads touch the variable at the same time. Without them, an operation as simple as count++ is really three separate machine instructions (load, increment, store), and two threads interleaving those instructions can lose an update. Atomics close that gap in hardware, using instructions like lock xadd or load-linked/store-conditional, instead of falling back to a lock that would put a waiting thread to sleep.

The C11 <stdatomic.h> and C++11 <atomic> headers expose this hardware support as a portable set of operations, roughly grouped into load/store, read-modify-write, and wait/notify. Every atomic operation also carries a memory-order, which controls how much the compiler and CPU are allowed to reorder surrounding non-atomic accesses around it. The examples below default to memory_order_seq_cst for simplicity; see memory-order for the weaker, faster alternatives.

Load and store

The simplest atomic operations just read or write a value without any computation. They prevent a torn read or torn write, the case where a load or store of a multi-byte value is split across two bus transactions and a concurrent writer's update becomes visible halfway through.

#include <stdatomic.h>
 
atomic_int counter = 0;
 
int read_counter(void) {
    return atomic_load(&counter);
}
 
void set_counter(int value) {
    atomic_store(&counter, value);
}

On most mainstream architectures, aligned loads and stores of a machine word are atomic anyway. What atomic_load/atomic_store add on top is the memory-order guarantee and a guarantee the compiler will not cache the value in a register across iterations, which matters for spin loops.

Test-and-set

Test-and-set reads a boolean flag and unconditionally sets it to true, returning the previous value. It is one of the oldest atomic primitives (available as a single instruction on many CPUs going back decades) and is enough to build a simple Spinlock: a thread spins until it observes the flag was previously false, meaning it just claimed the lock.

#include <stdatomic.h>
 
atomic_flag lock = ATOMIC_FLAG_INIT;
 
void spin_lock(void) {
    while (atomic_flag_test_and_set(&lock)) {
        /* spin */
    }
}
 
void spin_unlock(void) {
    atomic_flag_clear(&lock);
}

atomic_flag is deliberately the most minimal atomic type in the standard; unlike atomic_bool, it is guaranteed lock-free on every conforming implementation.

Exchange

Exchange (xchg) unconditionally writes a new value into a location and returns the old one, atomically. It is a strictly more general test-and-set: instead of always setting to true, it sets to any value the caller chooses.

#include <stdatomic.h>
 
atomic_int owner = 0;
 
int try_claim(int thread_id) {
    return atomic_exchange(&owner, thread_id);   // returns previous owner
}

Exchange is unconditional, which is both its strength and its weakness: it always overwrites the current value, even if another thread just wrote something the caller did not know about. When the update needs to depend on the current value, compare-and-swap is the right tool instead.

Compare-and-swap

Compare-and-swap (CAS) is the workhorse of lock-free programming. It compares a location's current value against an expected value, and only if they match does it write a new value; either way, it reports whether the swap happened. This turns “read, compute, write” into a single atomic step, at the cost of having to retry if another thread won the race.

#include <stdatomic.h>
 
atomic_int shared = 0;
 
void increment(void) {
    int expected = atomic_load(&shared);
    int desired;
    do {
        desired = expected + 1;
    } while (!atomic_compare_exchange_weak(&shared, &expected, desired));
}

C11 provides both atomic_compare_exchange_weak and _strong. The _weak form may fail spuriously even when the value matches, which is cheaper on LL/SC architectures (ARM, RISC-V) that cannot atomically compare-and-swap in one instruction; it is meant to be called in a retry loop like the one above. _strong never fails spuriously but pays for that guarantee with an internal retry loop of its own, so it is the right choice when the caller cannot easily retry.

Fetch-and-arithmetic

The fetch-and-add family (fetch_add, fetch_sub, and their increment/decrement special cases) atomically applies an arithmetic operation and returns the value from before the operation. These map directly to lock xadd on x86 and are cheaper than a CAS loop because the hardware guarantees success in one instruction.

#include <stdatomic.h>
 
atomic_int count = 0;
 
void record_event(void) {
    atomic_fetch_add(&count, 1);   // atomic_fetch_sub for decrement
}

Because the return value is the previous value, fetch-and-add doubles as a way to hand out unique sequence numbers: each caller gets a distinct value even under heavy contention, with no CAS retry loop needed.

Fetch-and-bitwise

Fetch-and-and, fetch-and-or, and fetch-and-xor apply a bitwise operation atomically, again returning the prior value. They are the natural tool for atomically setting, clearing, or flipping individual bits in a flags word shared across threads.

#include <stdatomic.h>
 
atomic_uint flags = 0;
 
void set_flag(unsigned bit) {
    atomic_fetch_or(&flags, 1u << bit);
}
 
void clear_flag(unsigned bit) {
    atomic_fetch_and(&flags, ~(1u << bit));
}

A common mistake is assuming a read-modify-set pair like flags |= mask is equivalent to atomic_fetch_or. It is not: the plain version reads, computes, and stores in three separate steps, leaving the same race window that atomics exist to close.

Fetch-and-reduce

Fetch-and-min and fetch-and-max atomically replace a value with the minimum or maximum of itself and an argument, returning the previous value. They are less universally available in hardware than add or bitwise ops (not all ISAs have a native atomic min/max), so on some platforms the compiler lowers them to a CAS retry loop under the hood.

#include <stdatomic.h>
 
atomic_int running_max = INT_MIN;
 
void observe(int value) {
    int prev = atomic_load(&running_max);
    while (value > prev &&
           !atomic_compare_exchange_weak(&running_max, &prev, value)) {
        /* prev is updated by the failed CAS; retry */
    }
}

This pattern, tracking a running extremum across threads without a lock, shows up often enough in reductions that some libraries expose it as atomic_fetch_max directly rather than making every caller write the CAS loop by hand.

Atomic flag and wait/notify

C11 and C++20 add wait/notify on top of atomics, letting a thread block until a value changes instead of busy-spinning. atomic_wait puts the calling thread to sleep until the observed value differs from a given one; atomic_notify_one and atomic_notify_all wake up one or all waiters respectively. This gives atomics the blocking behavior of a mutex and condition variable without needing a separate lock object.

#include <stdatomic.h>
 
atomic_int ready = 0;
 
void producer(void) {
    atomic_store(&ready, 1);
    atomic_notify_all(&ready);
}
 
void consumer(void) {
    int observed;
    while ((observed = atomic_load(&ready)) == 0) {
        atomic_wait(&ready, observed);
    }
}

Unlike a plain spin loop, atomic_wait allows the OS scheduler to park the waiting thread rather than burning CPU cycles, while still avoiding the overhead of a full mutex and condition variable pair.

Further reading

The operations above are the building blocks; several larger topics deserve their own articles. memory-order covers the ordering guarantees (relaxed through seq_cst) that every atomic operation above can be tagged with. Lock-free queue, Treiber stack, and Michael-Scott queue show how CAS loops compose into full lock-free data structures. ABA problem and Hazard pointer cover the memory-reclamation hazards that come with lock-free structures built on CAS.