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++11A 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_subwithsizeof(T)stride. - Floating-point types (C++20):
fetch_addandfetch_subadded; arithmetic is lock-free on most x86-64 and ARM64 platforms. std::shared_ptr<T>andstd::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
#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/2Examples
Memory orders
| Order | Guarantee | Typical use |
|---|---|---|
relaxed | Atomic only — no ordering on surrounding ops | Independent counters, statistics |
consume | Data-dependency ordering (promoted to acquire by all major compilers — avoid) | Avoid in new code |
acquire | All stores before a paired release are visible after this load | Consumer side of a flag or pointer |
release | All stores before this become visible to a paired acquire load | Producer side of a flag or pointer |
acq_rel | acquire + release on a single RMW | Middle of a producer-consumer chain |
seq_cst | Total order across all seq_cst operations globally | Default; correct everywhere, may add a fence |
Producer-consumer with acquire/release
#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
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().
#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>:
#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++20All 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:
#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 oneAtomic shared_ptr (C++20)
#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)
#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 threadsstd::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:
#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 barrierPrefer 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 —relaxedon the wrong operation causes heisenbugs that only appear on non-x86 hardware. - Use
compare_exchange_weakin retry loops,compare_exchange_strongfor 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. Usestd::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 rawshared_ptr. It provides lock-free reads on most platforms and has cleaner ownership semantics.
Common Pitfalls
Using relaxed for signaling between threads
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 resultThe 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
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 atomicIgnoring 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
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 thisIf this assert fires, rethink the design: consider std::atomic<Matrix4x4*> (pointer swap) or a mutex.
Forgetting that compare_exchange_* updates expected on failure
// 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.
// 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.
// 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.
// 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.
// 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.
// 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:
| Feature | atomic::wait / notify | std::condition_variable |
|---|---|---|
| Requires mutex | No | Yes (unique_lock) |
| Spurious wakeups | Yes (re-check built in) | Yes (user must loop) |
| Predicate support | Manual (check after wake) | wait(lock, pred) built in |
| Overhead | Lower — no mutex acquire | Higher — mutex + condvar |
| Multiple waiters | notify_all() | notify_all() |
| Best for | Single integer flag/phase | Complex 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.
// 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.
// 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.
// 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.
// 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
std::mutex— lock-based synchronization for complex critical sectionsstd::memory_order— complete C++ memory model referencestd::atomic_flag— guaranteed lock-free boolean primitivestd::atomic_ref— C++20 atomic operations over non-atomic storagestd::shared_ptr—std::atomic<shared_ptr<T>>specialization (C++20)std::condition_variable— blocking wait with predicate for complex synchronization