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

std::atomic

std::atomic<T> provides lock-free atomic operations with configurable memory ordering — covers memory orders, CAS loops, atomic_flag, atomic_ref, wait/notify, and fences.

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

A class template that wraps a value of type T and guarantees all reads and writes are indivisible — no thread observes a torn value — with configurable memory ordering semantics that control inter-thread visibility of surrounding non-atomic operations.

Overview

std::atomic<T> makes individual operations on T atomic, but atomicity alone is insufficient for correct concurrent programs. You also need to specify which non-atomic writes become visible when another thread performs an atomic load. That is what memory orders govern — they are the main complexity of this API, and the most common source of bugs.

Supported Types

std::atomic<T> accepts:

  • All integer types (bool, char, short, int, long, all unsigned variants, size_t, ptrdiff_t, fixed-width types from <cstdint>)
  • Pointer types (T*)
  • Floating-point types (float, double, long double) — available since C++11 via the primary template; fetch_add/fetch_sub added in C++20
  • Any trivially copyable type — may use internal locks if not natively lock-free
cpp
std::atomic<int>    ai{0};
std::atomic<int*>   ap{nullptr};
std::atomic<bool>   ab{false};
std::atomic<double> ad{0.0};      // C++11; fetch_add/fetch_sub require C++20

static_assert(std::atomic<int>::is_always_lock_free);   // C++17 compile-time check
bool rt = ai.is_lock_free();                            // C++11 runtime check

User-defined types: std::atomic<MyStruct> compiles if MyStruct is trivially copyable, trivially destructible, and memcpy-able (no virtual functions, no virtual bases). The implementation may use an internal mutex — is_always_lock_free tells you at compile time whether it does.


Memory Ordering

Every atomic operation accepts an optional std::memory_order argument. The default is seq_cst, which is the strongest and most expensive. Relaxed ordering bugs are nearly impossible to reproduce; loosen only with a correctness proof in hand.

OrderApplies toGuarantee
relaxedload, store, RMWAtomicity only; no inter-thread ordering
consumeloadData-dependency ordering (compilers promote to acquire; avoid in practice)
acquireload, RMWSees all stores sequenced before the pairing release
releasestore, RMWAll prior writes visible to any thread that acquire-loads this value
acq_relRMW onlyBoth acquire and release semantics
seq_cstallTotal order across all seq_cst operations in the program

The core contract: a release store to x synchronizes-with an acquire load from x that observes the stored value. Every non-atomic write sequenced before the release store becomes visible to the thread that performed the acquire load.

cpp
std::atomic<bool> ready{false};
int data = 0;

// Thread A — producer
data = 42;
ready.store(true, std::memory_order_release);   // publishes data

// Thread B — consumer
while (!ready.load(std::memory_order_acquire)); // acquire: sees data == 42
assert(data == 42);                             // guaranteed

relaxed is correct for counters whose final value is read after all threads complete — no ordering relative to other variables is needed:

cpp
std::atomic<int> hits{0};
// Per-thread:
hits.fetch_add(1, std::memory_order_relaxed);
// After std::thread::join() on all workers:
std::cout << hits.load(std::memory_order_relaxed) << '\n';

Load, Store, Exchange

cpp
std::atomic<int> x{42};

int val  = x.load(std::memory_order_acquire);
x.store(100, std::memory_order_release);
int prev = x.exchange(200);  // atomic swap — returns the value replaced

exchange is an atomic read-modify-write: even under contention, it returns exactly the value it replaced.


Compare-and-Swap

CAS is the primitive underlying nearly all lock-free algorithms. Atomically: if x == expected, set x = desired and return true; otherwise write the current value of x into expected and return false.

cpp
std::atomic<int> x{0};
int expected = 0;

// Strong — no spurious failure; for single-attempt checks
if (x.compare_exchange_strong(expected, 1)) {
    // x was 0, now 1
} else {
    // x was not 0; expected holds x's current value
}

// Weak — may spuriously fail; use in retry loops (better codegen on LL/SC hardware)
expected = 0;
while (!x.compare_exchange_weak(expected, 1)) {
    if (expected != 0) break;  // genuine mismatch
    // spurious failure — expected is unchanged, retry
}

compare_exchange_weak/strong accept two memory orders: the first for success, the second for failure. The failure order cannot be release or acq_rel, and cannot be stronger than the success order.

CAS loop for lock-free read-modify-write

cpp
std::atomic<int> value{5};
int old_val, new_val;
do {
    old_val = value.load(std::memory_order_relaxed);
    new_val = transform(old_val);
} while (!value.compare_exchange_weak(old_val, new_val,
                                       std::memory_order_acq_rel,
                                       std::memory_order_relaxed));

ABA problem: thread A reads value X, thread B changes it to Y then back to X, thread A's CAS succeeds incorrectly. Mitigate with a version tag packed alongside the value:

cpp
struct Tagged {
    uintptr_t ptr : 48;
    uintptr_t tag : 16;  // increment on every successful CAS
};
std::atomic<Tagged> head{};
// CAS on the whole Tagged struct; spurious success requires matching both ptr and tag

Fetch-and-Modify

cpp
std::atomic<int> x{10};

x.fetch_add(5);               // returns 10, x becomes 15
x.fetch_sub(3);               // returns 15, x becomes 12
x.fetch_and(0xFF);
x.fetch_or(0x01);
x.fetch_xor(0x0F);

++x; x++; x += 5; x &= mask; // operator shortcuts (all seq_cst)

// Floating-point fetch_add/fetch_sub — C++20
std::atomic<double> score{0.0};
score.fetch_add(1.5, std::memory_order_relaxed);  // C++20

All fetch_* operations return the value before modification.


std::atomic_flag

The only type guaranteed lock-free. In C++11–17, only test_and_set() and clear() exist. C++20 adds test(), wait(), and notify_*.

cpp
// C++11–17: requires ATOMIC_FLAG_INIT
std::atomic_flag flag = ATOMIC_FLAG_INIT;

// C++20: default-initialized to false; ATOMIC_FLAG_INIT is deprecated
std::atomic_flag flag2{};

class Spinlock {
    std::atomic_flag flag_{};  // C++20 default init
public:
    void lock() noexcept {
        while (flag_.test_and_set(std::memory_order_acquire))
            flag_.wait(true, std::memory_order_relaxed);  // C++20: park instead of burn CPU
    }
    void unlock() noexcept {
        flag_.clear(std::memory_order_release);
        flag_.notify_one();  // C++20
    }
};

bool current = flag2.test(std::memory_order_acquire);  // C++20: read without modifying

std::atomic_ref (C++20)

Applies atomic operations to a non-atomic object without changing its type. Ideal for elements of plain arrays and struct members that are accessed concurrently:

cpp
// C++20
int counters[1024]{};

void increment(int& elem) {
    std::atomic_ref<int> ref(elem);
    ref.fetch_add(1, std::memory_order_relaxed);
}

// Safe: each thread works on a distinct element
std::for_each(std::execution::par, std::begin(indices), std::end(indices),
    [&](int i) { increment(counters[i]); });

Critical constraint: while any atomic_ref to an object exists, all accesses to that object — including from other threads — must go through an atomic_ref. Direct non-atomic access is a data race. The object must also be aligned to std::atomic_ref<T>::required_alignment.


Wait and Notify (C++20)

wait() blocks efficiently until the atomic value changes — far better than polling:

cpp
std::atomic<int> state{0};

// Waiter
state.wait(0);                               // blocks until state != 0
process(state.load(std::memory_order_acquire));

// Notifier
state.store(1, std::memory_order_release);
state.notify_one();   // wake one; or notify_all()

wait(old_val) re-checks atomically on each wakeup — no lost notifications. Works on any std::atomic<T> and on std::atomic_flag:

cpp
std::atomic<bool> done{false};

std::jthread worker([&] {  // C++20 jthread
    do_work();
    done.store(true, std::memory_order_release);
    done.notify_one();
});

done.wait(false, std::memory_order_acquire);  // block until done != false

Fences

std::atomic_thread_fence (C++11) establishes ordering constraints decoupled from a specific atomic variable — useful to batch ordering for multiple atomics at once:

cpp
std::atomic<bool> ready{false};
int payload = 0;

// Producer
payload = 99;
std::atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed);   // relaxed — fence carries the sync

// Consumer
while (!ready.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
assert(payload == 99);  // visible: fence-fence pair synchronized

The release fence synchronizes-with the acquire fence when the acquire fence is sequenced after a load that reads the value stored after the release fence.


volatile vs std::atomic

These are orthogonal and must not be confused:

volatilestd::atomic
Prevents compiler caching accessesYesNo
Prevents reordering between threadsNoYes (order-dependent)
Guarantees atomicityNoYes
Suitable for inter-thread communicationNoYes
Use caseMMIO registers, setjmp variablesShared state between threads

volatile prevents the compiler from eliding or reordering reads and writes to a variable within a single thread — it says nothing about other threads. Using volatile to synchronize threads is undefined behavior. std::atomic is the correct tool for inter-thread communication.


Best Practices

  • Start with seq_cst, relax with profiling and proof. Relaxed-ordering bugs reproduce rarely, often only under specific hardware or load conditions.
  • Use compare_exchange_weak in retry loops. On LL/SC architectures (ARM, RISC-V), _strong internally retries; _weak lets you control the outer loop and merge it with your application logic.
  • Assert is_always_lock_free for custom types. If it evaluates to false, you may be holding a lock inside a lock, defeating the point of the atomic type.
  • Never write x = x + 1 on an atomic. The compound statement compiles to a non-atomic load + add + store. Use fetch_add or +=.
  • Pair release stores with acquire loads. A release store only synchronizes-with an acquire (or seq_cst) load. A relaxed load does not establish a happens-before edge.

Common Pitfalls

Composing atomics is not itself atomic. Individually atomic operations do not compose. If you need x and y to change atomically together, use a mutex.

Double-checked locking requires correct orders:

cpp
std::atomic<Widget*> ptr_{nullptr};
std::mutex mtx_;

Widget* get() {
    // Wrong: relaxed load; constructed Widget might not be visible
    // Widget* p = ptr_.load(std::memory_order_relaxed);

    Widget* p = ptr_.load(std::memory_order_acquire);   // correct
    if (!p) {
        std::lock_guard lk(mtx_);
        p = ptr_.load(std::memory_order_relaxed);        // under mutex — relaxed is fine
        if (!p) {
            p = new Widget();
            ptr_.store(p, std::memory_order_release);    // publish
        }
    }
    return p;

    // Simpler alternative — guaranteed thread-safe in C++11:
    // static Widget instance;
    // return &instance;
}

atomic_ref lifetime violations: the backing object must outlive all atomic_ref instances that refer to it, and must not be accessed non-atomically while any such atomic_ref exists.

Spinlocks without pause: tight test_and_set loops on x86 cause cache-line contention. Insert _mm_pause() (x86 intrinsic) or use atomic_flag::wait() (C++20) to yield to the OS.


Quick Reference

OperationMemberReturnsSince
Readload(order)TC++11
Writestore(val, order)voidC++11
Atomic swapexchange(val, order)old TC++11
CAS (no spurious fail)compare_exchange_strong(exp, val)boolC++11
CAS (retry loops)compare_exchange_weak(exp, val)boolC++11
Add, get oldfetch_add(n, order)old TC++11
Subtract, get oldfetch_sub(n, order)old TC++11
Bitwise AND, get oldfetch_and(mask, order)old TC++11
Bitwise OR, get oldfetch_or(bits, order)old TC++11
Bitwise XOR, get oldfetch_xor(bits, order)old TC++11
Test flag (no set)test(order) (atomic_flag)boolC++20
Block until ≠ valwait(val, order)voidC++20
Wake one waiternotify_one()voidC++20
Wake all waitersnotify_all()voidC++20

compare_exchange_weak vs compare_exchange_strong

Choosing between the two variants is not just style — it directly affects correctness and performance on certain hardware. On Load-Linked / Store-Conditional architectures (ARM, RISC-V, PowerPC), the hardware CAS is inherently a two-instruction sequence that can fail spuriously when an unrelated store evicts the cache line between the LL and the SC. compare_exchange_weak exposes this failure directly to the caller; compare_exchange_strong hides it by retrying internally.

When you already have a retry loop — as in the lock-free read-modify-write idiom — _weak lets the spurious-failure retry merge naturally with your loop, avoiding a redundant inner retry:

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

// Prefer _weak inside an explicit retry loop
int old_val, new_val;
do {
    old_val = counter.load(std::memory_order_relaxed);
    new_val = old_val + expensive_compute(old_val);
    // spurious failure is harmless — we just reload and retry
} while (!counter.compare_exchange_weak(old_val, new_val,
                                         std::memory_order_acq_rel,
                                         std::memory_order_relaxed));

Use compare_exchange_strong when you perform only a single attempt with no surrounding loop — for example, a try-once ownership grab — because you need to distinguish a genuine mismatch from hardware noise:

cpp
// C++11
std::atomic<bool> claimed{false};
bool expected = false;

// No retry loop: _strong guarantees no spurious false returns
if (claimed.compare_exchange_strong(expected, true,
                                    std::memory_order_acq_rel,
                                    std::memory_order_relaxed)) {
    // this thread exclusively owns the resource
}

On x86 the two variants compile to the same CMPXCHG instruction, so the choice is mostly a documentation signal there. On ARM the difference is real: _weak can emit a single STXR without a surrounding branch.


Atomic Floating-Point Operations (C++20)

std::atomic<float> and std::atomic<double> have been valid since C++11, but fetch_add and fetch_sub were only standardised in C++20. Before C++20, floating-point atomics only support load, store, and exchange; arithmetic required a manual CAS loop. C++20 fills this gap for the common pattern of accumulating a running sum from multiple threads.

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

std::atomic<float> running_loss{0.0f};

void update_loss(float batch_loss) {
    // fetch_add returns the value before addition
    running_loss.fetch_add(batch_loss, std::memory_order_relaxed);
}

float get_loss() {
    return running_loss.load(std::memory_order_relaxed);
}

relaxed ordering is appropriate here because the application only reads the total after all worker threads have joined — no ordering relative to other variables is required. Note that floating-point addition is not associative, so the final accumulated value may differ from a sequential sum due to reordering; this is acceptable for statistical accumulators but not for deterministic numeric computations.

For a pre-C++20 workaround using a CAS loop, the fetch_add can be emulated:

cpp
// C++11 workaround for float fetch_add
std::atomic<float> total{0.0f};

void add_to_total(float delta) {
    float old_val = total.load(std::memory_order_relaxed);
    float new_val;
    do {
        new_val = old_val + delta;
    } while (!total.compare_exchange_weak(old_val, new_val,
                                          std::memory_order_relaxed,
                                          std::memory_order_relaxed));
}

Seqlock Pattern

A seqlock lets readers proceed without acquiring a lock, at the cost of retrying when a write is in progress — ideal for frequently-read, rarely-written data like timestamps or statistics. The writer increments a sequence counter before and after modifying the payload; a reader snapshots the counter, copies the payload, then re-reads the counter. If the two counter values differ — or if either is odd — a write was concurrent with the read and the reader retries.

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

struct Seqlock {
    std::atomic<uint32_t> seq{0};

    // Payload — keep it small (fits a cache line)
    double x{0.0};
    double y{0.0};

    void write(double nx, double ny) {
        uint32_t s = seq.load(std::memory_order_relaxed);
        seq.store(s + 1, std::memory_order_release);  // odd → write in progress
        x = nx;
        y = ny;
        seq.store(s + 2, std::memory_order_release);  // even → write complete
    }

    bool try_read(double& out_x, double& out_y) const {
        uint32_t s1 = seq.load(std::memory_order_acquire);
        if (s1 & 1) return false;  // write in progress
        out_x = x;
        out_y = y;
        uint32_t s2 = seq.load(std::memory_order_acquire);
        return s1 == s2;           // true only if no write occurred during read
    }

    void read(double& out_x, double& out_y) const {
        while (!try_read(out_x, out_y)) { /* retry */ }
    }
};

The acquire loads on the sequence counter prevent the compiler and CPU from hoisting the payload reads above the first counter read or sinking them below the second. Writers must ensure stores to x and y are visible before the even counter store — the release on the final seq.store achieves this. For payloads larger than a pointer, prefer std::shared_mutex over a seqlock; the retry cost grows with payload size and write frequency.


std::atomic_ref In Depth (C++20)

std::atomic_ref<T> retrofits atomic semantics onto an already-existing non-atomic object — no change to the object's type or storage is required. This is the right tool when you have a plain array, a legacy struct, or any contiguous buffer that you want to access atomically in a narrow scope without permanently changing the object's layout.

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

int main() {
    std::vector<int> buckets(256, 0);

    auto worker = [&](int start, int end) {
        for (int i = start; i < end; ++i) {
            int bucket = i % 256;
            std::atomic_ref<int> ref(buckets[bucket]);
            ref.fetch_add(1, std::memory_order_relaxed);
        }
    };

    std::thread t1(worker, 0, 50000);
    std::thread t2(worker, 50000, 100000);
    t1.join();
    t2.join();
    // Sum across buckets is exactly 100000
}

atomic_ref supports the same full interface as std::atomic<T>: load, store, exchange, compare_exchange_weak/strong, and all fetch_* operations. It inherits is_always_lock_free from std::atomic<T> for the same type.

Several invariants must hold for correct use. First, the referenced object must be aligned to std::atomic_ref<T>::required_alignment, which may be stricter than alignof(T). Second, for the entire lifetime of any atomic_ref to an object, every access to that object — in every thread — must go through an atomic_ref; direct non-atomic access is a data race even if the current thread holds no atomic_ref locally. Third, the backing object must outlive all atomic_ref instances that refer to it.

cpp
// C++20 — struct member example
struct Counters {
    int hits;     // must be aligned (usually fine for int)
    int misses;
};

void record_hit(Counters& c) {
    std::atomic_ref<int>(c.hits).fetch_add(1, std::memory_order_relaxed);
}

void record_miss(Counters& c) {
    std::atomic_ref<int>(c.misses).fetch_add(1, std::memory_order_relaxed);
}

When the accessed field is always updated through atomic_ref, this is well-defined and avoids the overhead of making the entire struct atomic.


See Also

  • std::mutex — prefer when CAS retry loops grow complex or contention is high
  • std::memory_order — full semantics and happens-before rules
  • std::shared_ptr — uses atomic operations internally for the reference count
  • RAII — wrap lock acquisition in a scope guard for exception safety