Guarantees of std::atomic in C++ Concurrency
This article examines the core guarantees provided by
std::atomic types within the C++ memory model. Introduced
in C++11, std::atomic templates allow safe, concurrent
access to shared variables without the overhead of heavy mutual
exclusion primitives like mutexes. Understanding these
guarantees—spanning operation atomicity, data race elimination,
modification order consistency, and configurable memory ordering—is
fundamental for writing robust, high-performance multithreaded
software.
1. Atomicity of Operations
The primary guarantee of std::atomic is indivisibility.
Any read (load), write (store), or read-modify-write (RMW) operation
performed on an atomic object is executed completely or not at all:
- No Torn Reads or Writes: In non-atomic types, writing a multi-byte variable (such as a 64-bit integer on a 32-bit architecture) can result in a torn state where another thread reads partial, corrupt data. Atomic types guarantee that values are updated and observed as whole units.
- Atomic Read-Modify-Write (RMW): Compound operations
such as
fetch_add,fetch_sub,exchange, andcompare_exchange_weak/compare_exchange_strongare executed atomically at the hardware level, preventing lost updates when multiple threads mutate the same variable simultaneously.
2. Elimination of Data Races
Under the C++ standard, two memory accesses conflict if they target the same memory location, at least one is a write, and neither is an atomic operation. A conflict without synchronization constitutes a data race, which results in undefined behavior (UB).
Using std::atomic guarantees that concurrent accesses to
the same object are well-defined and free from data races. Compilers and
processors are constrained from performing optimizations (such as
register caching or instruction reordering) that would invalidate the
visibility or validity of the atomic variable.
3. Total Modification Order
Every individual atomic object possesses a strictly defined modification order (coherence order). This provides four critical sub-guarantees:
- Write-Write Coherence: All threads observe writes to a specific atomic object in the exact same sequence.
- Read-Read Coherence: If a thread reads a value \(A\) from an atomic object and later reads a value \(B\), \(A\) cannot appear after \(B\) in that object's modification order.
- Read-Write Coherence: If a thread reads a value, it cannot subsequently read an older value overwritten by that write.
- Write-Read Coherence: A write cannot overwrite a value that a subsequent read on the same thread observes.
While two distinct atomic variables may not have a globally unified order unless explicitly specified, any single atomic variable is universally agreed upon by all CPU cores regarding its state transitions.
4. Configurable Memory Ordering and Synchronization
Beyond local atomicity, atomic operations govern how surrounding
non-atomic memory accesses are ordered and made visible across threads.
C++ provides six memory ordering models through
std::memory_order:
- Sequentially Consistent
(
memory_order_seq_cst): The default ordering. It enforces a single, globally agreed-upon interleaving of operations across all threads. No memory access can be reordered across a sequentially consistent operation. - Acquire-Release Semantics
(
memory_order_acquire,memory_order_release,memory_order_acq_rel):- A store operation with
memory_order_releasesynchronizes with a load operation usingmemory_order_acquirethat reads the stored value. - Memory writes occurring before the release store in the writing thread are guaranteed to be visible to the reading thread after the acquire load.
- This establishes a formal "happens-before" relationship without enforcing total global ordering.
- A store operation with
- Relaxed Ordering
(
memory_order_relaxed): Guarantees atomicity and modification order for the specific atomic variable, but provides no synchronization or ordering guarantees relative to any surrounding memory operations.
5. Lock-Free Guarantees
Atomic operations may be implemented directly via hardware instructions (lock-free) or internally via compiler-generated mutexes/locks. C++ provides mechanisms to inspect these guarantees:
- Compile-Time Checks: Macros such as
ATOMIC_INT_LOCK_FREEindicate whether operations on specific types are always lock-free. - Runtime Verification: The member function
is_lock_free()returnstrueif the operations on a specific atomic instance are handled without internal locks. std::atomic_flag: The only type guaranteed by the C++ standard to be strictly lock-free across all compliant implementations, commonly used as a building block for spinlocks.