Skip to content
C++
Library
since C++11
Advanced

std::atomic

Thread-safe atomic operations without mutexes — memory orders, CAS loops, atomic_flag, atomic_ref (C++20), wait/notify (C++20), and lock-free patterns.

std::atomic<T>since C++11

A class template that wraps a single object of type T and guarantees that all operations on it — loads, stores, and read-modify-write sequences — are indivisible from any other thread's perspective, with configurable memory ordering that controls which surrounding memory operations become visible across a synchronization point.

Overview

std::atomic<T> (<atomic>, C++11) delivers three things a mutex also gives you — atomicity, visibility, and ordering — but using hardware-level instructions (CAS, LL/SC, memory fences) rather than OS primitives. The result is lower overhead for small, frequent updates.

atomic is not volatile. volatile tells the compiler not to optimize away accesses to memory-mapped I/O or signal-handler-visible variables. It carries no thread-safety or ordering guarantee. std::atomic is entirely about multi-thread correctness; volatile std::atomic<T> is legal but the volatile qualifier adds nothing useful in a concurrent context.

Which types can be used

The primary template std::atomic<T> accepts any T that is trivially copyable with no padding bits that affect object identity. This covers all integral types, pointers, floating-point types, and small plain structs. Specializations with extended APIs:

  • Integral types: full arithmetic (fetch_add, fetch_sub) and bitwise (fetch_and, fetch_or, fetch_xor) plus ++/--.
  • Pointer types (T*): fetch_add/fetch_sub with sizeof(T) stride.
  • Floating-point types (C++20): fetch_add and fetch_sub added; arithmetic is lock-free on most x86-64 and ARM64 platforms.
  • std::shared_ptr<T> and std::weak_ptr<T> (C++20): dedicated specializations replace the deprecated C++11/17 free-function API (std::atomic_load, std::atomic_store).

For types too large or complex for hardware atomic instructions, the implementation may use a hidden mutex — is_lock_free() (runtime) and is_always_lock_free (compile-time, C++17) let you verify.

Syntax

cpp
#include <atomic>

// Construction — not copy-constructible; use direct or list initialization
std::atomic<int> i{0};           // C++11
std::atomic<long long> ll{0LL};  // C++11

// Core operations — all accept an optional std::memory_order (default: seq_cst)
int  val = i.load();             // atomic read
i.store(42);                     // atomic write
int  old = i.exchange(99);       // atomic read-modify-write: returns old value

// Compare-and-swap (CAS) — the building block of lock-free algorithms
int expected = 5;
bool won = i.compare_exchange_strong(expected, 10);
// i == 5: writes 10, returns true
// i != 5: writes current i into expected, returns false — no manual reload needed

bool also_won = i.compare_exchange_weak(expected, 10);
// Same semantics; may spuriously fail even when i == expected
// Use in retry loops; preferred on LL/SC architectures (ARM, POWER)

// Arithmetic — integral and pointer specializations only
i.fetch_add(1);   // atomic add, returns old value
i.fetch_sub(1);   // atomic subtract, returns old value
++i; i++;         // equivalent; ++i returns new value, i++ returns old

// Bitwise — integral specializations only
i.fetch_and(0xFF);
i.fetch_or(0x01);
i.fetch_xor(0xFF);

// Lock-free introspection
bool rt = i.is_lock_free();                             // C++11, runtime
constexpr bool ct = std::atomic<int>::is_always_lock_free; // C++17, compile-time
static_assert(ATOMIC_INT_LOCK_FREE == 2);               // C++11 macro: 0/1/2

Examples

Memory orders

OrderGuaranteeTypical use
relaxedAtomic only — no ordering on surrounding opsIndependent counters, statistics
consumeData-dependency ordering (promoted to acquire by all major compilers — avoid)Avoid in new code
acquireAll stores before a paired release are visible after this loadConsumer side of a flag or pointer
releaseAll stores before this become visible to a paired acquire loadProducer side of a flag or pointer
acq_relacquire + release on a single RMWMiddle of a producer-consumer chain
seq_cstTotal order across all seq_cst operations globallyDefault; correct everywhere, may add a fence

Producer-consumer with acquire/release

cpp
#include <atomic>
#include <thread>

int data = 0;
std::atomic<bool> ready{false};  // C++11

void producer() {
    data = 42;                                             // plain write — happens-before release
    ready.store(true, std::memory_order_release);          // "data is published"
    // Guarantee: data = 42 is not reordered past this store
}

void consumer() {
    while (!ready.load(std::memory_order_acquire))         // "wait for publication"
        std::this_thread::yield();
    // Guarantee: data == 42 here — the acquire-release pair creates a happens-before edge
    use(data);
}

The release store acts as a one-way barrier: everything written before it stays before it. The acquire load is the mirror: nothing read after it is reordered before it. Together they form the synchronization contract.

CAS loop for lock-free update

cpp
std::atomic<int> value{10};  // C++11

// Atomically apply an arbitrary transformation — no lock required
template<typename F>
int atomic_update(std::atomic<int>& a, F transform) {
    int old = a.load(std::memory_order_relaxed);
    int desired;
    do {
        desired = transform(old);
    } while (!a.compare_exchange_weak(
        old,     desired,
        std::memory_order_acq_rel,   // success: full barrier
        std::memory_order_relaxed)); // failure: re-read only
    return old;
}

atomic_update(value, [](int v) { return v * 2; });

On failure, compare_exchange_weak writes the current atomic value back into old — no manual reload. Use compare_exchange_weak in loops (maps to a single LL/SC pair on ARM); use compare_exchange_strong for single-shot attempts where retrying isn't appropriate.

atomic_flag — guaranteed lock-free boolean

std::atomic_flag is the only type the standard guarantees is always lock-free. C++11 provided only test_and_set() and clear(). C++20 added test(), wait(), notify_one(), and notify_all().

cpp
#include <atomic>

class SpinLock {
    std::atomic_flag flag_{};  // C++20: value-init guarantees cleared state
                               // C++11/14/17: requires = ATOMIC_FLAG_INIT or {}
public:
    void lock() noexcept {
        while (flag_.test_and_set(std::memory_order_acquire)) {
#if defined(__x86_64__)
            __builtin_ia32_pause();   // PAUSE reduces pipeline stalls on x86
#elif defined(__aarch64__)
            asm volatile("yield");    // WFE hint on ARM
#endif
        }
    }
    void unlock() noexcept {
        flag_.clear(std::memory_order_release);
    }
    bool is_locked() const noexcept {
        return flag_.test(std::memory_order_relaxed);  // C++20
    }
};

std::atomic_ref (C++20)

Apply atomic operations to existing non-atomic storage — for array elements, struct members, or C-ABI–compatible buffers you cannot change to std::atomic<T>:

cpp
#include <atomic>
#include <vector>
#include <cstdint>

std::vector<int> histogram(256, 0);

void tally(const std::vector<uint8_t>& data) {
    for (uint8_t byte : data) {
        // atomic_ref<T> requires alignment >= atomic_ref<T>::required_alignment
        std::atomic_ref<int>{histogram[byte]}           // C++20
            .fetch_add(1, std::memory_order_relaxed);
    }
}

// If storing data on the stack or in a struct, enforce alignment explicitly:
alignas(std::atomic_ref<int>::required_alignment) int shared{0};  // C++20

All concurrent access to the same storage must go through atomic_ref — mixing direct reads/writes with atomic_ref operations is undefined behavior.

Wait / notify (C++20)

Efficient blocking until an atomic changes — uses futex on Linux, WaitOnAddress on Windows. Far cheaper than a spin loop:

cpp
#include <atomic>

std::atomic<int> phase{0};  // C++11

// Worker thread
phase.wait(0, std::memory_order_acquire);  // C++20: block while phase == 0
// Spurious wakeups are safe — wait() re-checks the value atomically
do_work(phase.load(std::memory_order_acquire));

// Coordinator thread
phase.store(1, std::memory_order_release);
phase.notify_all();   // C++20: wake all waiters
// phase.notify_one() to wake exactly one

Atomic shared_ptr (C++20)

cpp
#include <atomic>
#include <memory>

// C++20: std::atomic<shared_ptr<T>> — dedicated specialization
std::atomic<std::shared_ptr<Config>> g_config;   // C++20

// Writer — replaces config atomically; old Config stays alive until all readers drop it
void update(std::shared_ptr<Config> next) {
    g_config.store(std::move(next), std::memory_order_release);
}

// Reader — snapshot; holds reference independent of future updates
auto cfg = g_config.load(std::memory_order_acquire);

The deprecated C++11 free functions (std::atomic_load, std::atomic_store on shared_ptr) still compile through C++23 but are removed in C++26.

Cache-line padding to prevent false sharing (C++17)

cpp
#include <new>   // std::hardware_destructive_interference_size  (C++17)
#include <array>

struct alignas(std::hardware_destructive_interference_size) ShardedCounter {
    std::atomic<uint64_t> value{0};
    // Padding to fill the rest of the cache line is implicit via alignas
};

std::array<ShardedCounter, 8> counters;
// Thread N writes counters[N].value — no cache-line ping-pong between threads

std::hardware_destructive_interference_size is 64 bytes on x86-64 and 128 bytes on Apple Silicon (M-series).

Standalone memory fences

When you need ordering across multiple variables without a single atomic to attach it to:

cpp
#include <atomic>

// Equivalent to release semantics on an imaginary store
std::atomic_thread_fence(std::memory_order_release);   // C++11; full CPU barrier

// Prevents only compiler reordering — for signal-handler synchronization
std::atomic_signal_fence(std::memory_order_acq_rel);   // C++11; no CPU barrier

Prefer attaching orders directly to atomic operations. Standalone fences are harder to audit and rarely necessary.

Best Practices

  • Default to seq_cst, optimize with evidence. It is correct for all use cases. Profile before relaxing — relaxed on the wrong operation causes heisenbugs that only appear on non-x86 hardware.
  • Use compare_exchange_weak in retry loops, compare_exchange_strong for one-shot attempts. Weak maps to a single LL/SC instruction on ARM/POWER; strong adds a retry loop internally.
  • Do not cast raw memory to std::atomic<T>* to force atomic access — it is undefined behavior. Use std::atomic_ref<T> (C++20) instead.
  • Verify lock-freedom with static_assert(std::atomic<T>::is_always_lock_free) (C++17) on any type where the latency guarantee matters.
  • Pad hot per-thread counters with alignas(std::hardware_destructive_interference_size) (C++17) to prevent false sharing.
  • Prefer std::atomic<shared_ptr<T>> (C++20) over a mutex protecting a raw shared_ptr. It provides lock-free reads on most platforms and has cleaner ownership semantics.

Common Pitfalls

Using relaxed for signaling between threads

cpp
int result = 0;
std::atomic<bool> done{false};

// Producer
result = expensive_compute();
done.store(true, std::memory_order_relaxed);  // WRONG: result may not be visible

// Consumer
while (!done.load(std::memory_order_relaxed)) {}
use(result);  // undefined behavior: no happens-before edge for result

The fix: memory_order_release on the store, memory_order_acquire on the load. relaxed is only safe when the atomic operation itself carries no payload — a standalone counter that no other data depends on.

Confusing volatile with atomic

cpp
volatile int counter = 0;
// Two threads writing counter is still a data race — volatile prevents
// compiler optimization but generates no atomic instruction or memory fence

std::atomic<int> safe_counter{0};
// Compiles to LOCK XADD / LOCK INC on x86 — genuinely atomic

Ignoring the ABA problem in lock-free pop

The lock-free stack pop shown in many examples has an ABA hazard: thread A reads head == X, thread B pops X, allocates Y, frees X, allocates a new node that lands at the same address X, and pushes it. Thread A's CAS on head succeeds, but X->next may now be garbage. Production mitigations: hazard pointers, epoch-based reclamation (e.g., folly::hazptr), or tagged pointers that pack a version counter into the spare pointer bits.

Assuming large structs are lock-free

cpp
struct Matrix4x4 { float data[16]; };
std::atomic<Matrix4x4> m{};
// Almost certainly implemented with a hidden mutex — not lock-free
static_assert(std::atomic<Matrix4x4>::is_always_lock_free);  // C++17 — fails, catches this

If this assert fires, rethink the design: consider std::atomic<Matrix4x4*> (pointer swap) or a mutex.

Forgetting that compare_exchange_* updates expected on failure

cpp
// Redundant and slower:
while (!a.compare_exchange_weak(expected, desired, ...)) {
    expected = a.load();  // unnecessary — CAS already wrote current value into expected
}

std::atomic_flag — The Only Guaranteed Lock-Free Atomic

std::atomic_flag is the single type the C++ standard requires to be always lock-free. Every other std::atomic<T> specialization may fall back to a hidden mutex on a given platform, but atomic_flag must not. Because of this minimal guarantee, C++11 deliberately kept its interface tiny: only test_and_set() and clear(). C++20 extended it with test(), wait(), notify_one(), and notify_all().

The classic use is a spinlock. A spin loop is appropriate for critical sections that complete in a handful of nanoseconds; for longer waits, combine atomic_flag with the C++20 wait/notify API shown in the next section.

cpp
// C++20
#include <atomic>
#include <thread>

class SpinLock {
    std::atomic_flag flag_{};   // value-init = cleared; C++11/14/17 need ATOMIC_FLAG_INIT
public:
    void lock() noexcept {
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // Back-off hint: avoid hammering the cache line
            // x86: reduces pipeline stalls during contention
            // ARM: hints the core to yield its pipeline slot
#if defined(__x86_64__)
            __builtin_ia32_pause();
#elif defined(__aarch64__)
            asm volatile("yield");
#else
            std::this_thread::yield();
#endif
        }
    }

    void unlock() noexcept {
        flag_.clear(std::memory_order_release);
    }

    // C++20: non-destructive read — does not set the flag
    bool is_locked() const noexcept {
        return flag_.test(std::memory_order_relaxed);
    }
};

// Usage
SpinLock sl;
{
    std::lock_guard<SpinLock> guard(sl);   // works with RAII wrappers
    // ... critical section ...
}

The test_and_set + acquire / clear + release pairing is the canonical idiom. The acquire on entry prevents the protected work from floating upward; the release on exit prevents it from floating downward. Omitting either ordering converts the spinlock into a data race.

cpp
// C++20 — parking a spinlock with wait/notify instead of a pure spin
#include <atomic>

class ParkingSpinLock {
    std::atomic_flag flag_{};
public:
    void lock() noexcept {
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // Block until flag_ is likely cleared; avoids burning CPU on long waits
            flag_.wait(true, std::memory_order_relaxed);
        }
    }
    void unlock() noexcept {
        flag_.clear(std::memory_order_release);
        flag_.notify_one();   // C++20: wake one parked thread
    }
};

wait(true) suspends the calling thread as long as the flag reads true, avoiding a tight polling loop. The OS reschedules the thread when notify_one() is called by the unlocker.

std::atomic<std::shared_ptr<T>> (C++20) — Lock-Free Read-Copy-Update

C++20 introduced dedicated std::atomic specializations for shared_ptr and weak_ptr, replacing the deprecated free-function API. Before C++20, code had to call std::atomic_load/std::atomic_store free functions, which are removed in C++26. The new specialization provides the same full atomic member-function interface and is lock-free for the read path on most platforms.

The dominant pattern is read-copy-update (RCU): readers snapshot the shared pointer with a single atomic load, work with their private copy, and never block writers. Writers atomically swap in a new allocation.

cpp
// C++20
#include <atomic>
#include <memory>

struct Config {
    int timeout_ms;
    std::string endpoint;
};

// Shared, concurrently readable configuration
std::atomic<std::shared_ptr<Config>> g_config;

// Writer — called rarely (config reload)
void update_config(int timeout_ms, std::string endpoint) {
    auto next = std::make_shared<Config>(timeout_ms, std::move(endpoint));
    g_config.store(std::move(next), std::memory_order_release);
    // Old Config lives until all readers holding a snapshot drop it
}

// Reader — called on every request
void handle_request() {
    // Acquire load: guaranteed to see all stores before the release
    auto cfg = g_config.load(std::memory_order_acquire);
    // cfg is a stable, private snapshot — safe to use after g_config is updated again
    connect(cfg->endpoint, cfg->timeout_ms);
}

compare_exchange is available too, enabling atomic config replacement only when a known version is still current — a pattern useful for optimistic concurrency control.

cpp
// C++20 — conditional update (optimistic replace)
void replace_if_unchanged(std::shared_ptr<Config> expected,
                          std::shared_ptr<Config> next) {
    // CAS: only stores next if g_config still holds expected
    g_config.compare_exchange_strong(expected, std::move(next),
                                     std::memory_order_acq_rel,
                                     std::memory_order_acquire);
}

The deprecated free functions (std::atomic_load(ptr), std::atomic_store(ptr, val)) still exist through C++23 for migration purposes. New code should use the specialization exclusively.

atomic::wait / notify_one / notify_all (C++20) — Futex-Based Blocking

C++20 added wait, notify_one, and notify_all to every std::atomic<T> specialization, providing OS-assisted blocking without a separate mutex or condition variable. The implementation typically calls futex on Linux and WaitOnAddress on Windows — the thread parks in the kernel and consumes no CPU while sleeping, unlike a spin loop.

wait(old) blocks the calling thread as long as the atomic value compares equal to old. When the value changes, any thread calling notify_one() or notify_all() wakes parked threads, which then re-read and return if the value differs from old. Spurious wakeups are permitted; the re-check is automatic and correct.

cpp
// C++20
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> phase{0};

void worker(int id) {
    // Phase 0: setup
    setup(id);

    // Wait until coordinator signals phase 1
    phase.wait(0, std::memory_order_acquire);  // blocks while phase == 0

    // Phase 1: run
    run(id);
}

void coordinator(int n_workers) {
    initialize_shared_state();

    // Signal all workers simultaneously
    phase.store(1, std::memory_order_release);
    phase.notify_all();   // wake every thread parked in wait(0)
}

Comparing wait/notify against std::condition_variable:

Featureatomic::wait / notifystd::condition_variable
Requires mutexNoYes (unique_lock)
Spurious wakeupsYes (re-check built in)Yes (user must loop)
Predicate supportManual (check after wake)wait(lock, pred) built in
OverheadLower — no mutex acquireHigher — mutex + condvar
Multiple waitersnotify_all()notify_all()
Best forSingle integer flag/phaseComplex shared state

For simple "ready" flags or phase counters, atomic::wait/notify is the cleaner choice. For predicates that involve several variables, condition_variable + mutex remains appropriate.

cpp
// C++20 — notify_one for producer/consumer queue depth signal
#include <atomic>

std::atomic<int> queue_size{0};

void producer() {
    enqueue_item();
    queue_size.fetch_add(1, std::memory_order_release);
    queue_size.notify_one();   // wake one waiting consumer
}

void consumer() {
    while (true) {
        queue_size.wait(0, std::memory_order_acquire);  // sleep while empty
        if (queue_size.fetch_sub(1, std::memory_order_acq_rel) > 0)
            process(dequeue_item());
        // else: spurious or raced — loop back and wait again
    }
}

is_lock_free() and ATOMIC_*_LOCK_FREE Macros

Lock-freedom is platform-dependent, and assuming it on types larger than a pointer is a common source of latency surprises. The C++ standard provides three mechanisms to check, with different granularity and evaluation time.

ATOMIC_*_LOCK_FREE macros (C++11) are the coarsest tool: they expand to 0 (never lock-free), 1 (sometimes, depending on alignment), or 2 (always lock-free) for each integral category. They are evaluated by the preprocessor and reflect what the platform/ABI supports in general.

cpp
// C++11
#include <atomic>
#include <cstdint>

// Macro values: 0 = never, 1 = sometimes, 2 = always
static_assert(ATOMIC_BOOL_LOCK_FREE    == 2);   // x86-64: always true
static_assert(ATOMIC_INT_LOCK_FREE     == 2);
static_assert(ATOMIC_LONG_LOCK_FREE    == 2);
static_assert(ATOMIC_LLONG_LOCK_FREE   == 2);   // 64-bit: always; 32-bit: 1 or 2
static_assert(ATOMIC_POINTER_LOCK_FREE == 2);

// ATOMIC_CHAR8_T_LOCK_FREE, ATOMIC_CHAR16_T_LOCK_FREE,
// ATOMIC_CHAR32_T_LOCK_FREE, ATOMIC_WCHAR_T_LOCK_FREE also available (C++11)

is_always_lock_free (C++17 constexpr static member) is more precise: it reflects the specific type T including its size and alignment, evaluated at compile time.

cpp
// C++17
#include <atomic>

static_assert(std::atomic<int>::is_always_lock_free);       // passes on x86-64
static_assert(std::atomic<double>::is_always_lock_free);    // C++20: passes on x86-64 with SSE2

struct Big { char data[32]; };
static_assert(!std::atomic<Big>::is_always_lock_free);      // fails — hidden mutex

// Use is_always_lock_free to guard latency-sensitive code paths
template<typename T>
void publish(std::atomic<T>& a, T val) {
    static_assert(std::atomic<T>::is_always_lock_free,
        "atomic<T> uses a mutex on this platform — reconsider the design");
    a.store(val, std::memory_order_release);
}

is_lock_free() (C++11 runtime member) accounts for runtime alignment, which matters on platforms where ATOMIC_INT_LOCK_FREE == 1 (sometimes). If is_always_lock_free is true, is_lock_free() is also always true; the runtime call is then redundant.

cpp
// C++11 — runtime check for dynamically allocated objects
#include <atomic>
#include <cassert>

void check_atomic_alignment() {
    // Misaligned atomic may fall back to a mutex even on an always-lock-free type
    alignas(std::atomic<int>) char buf[sizeof(std::atomic<int>)];
    auto* a = new (buf) std::atomic<int>(0);
    assert(a->is_lock_free());   // runtime: checks actual address alignment

    // Contrast: is_always_lock_free ignores instance alignment
    static_assert(std::atomic<int>::is_always_lock_free);   // C++17
}

As a rule: use ATOMIC_*_LOCK_FREE macros only in C-compatible headers or #if guards; prefer is_always_lock_free in static_assert in C++17+ code; fall back to is_lock_free() only when placement or dynamic allocation puts alignment in question.

See Also