C++ std::atomic
Dictionary meaning: Atomic means something that cannot be broken down
into smaller parts
atomic variable can only be changed/updated by one thread at a time.
atomic variable can be shared between threads and can be updated
(without the need for locks).
Only varibles can be declared as atomic, not functions or classes.
We cannot protect a multi-statement critical
section using a single std::atomic the way you use a std::mutex
Same idea as
Go atomics.
#include <atomic>
std::atomic<int> x{0};
Atomic Internal Implementation
Compiler injects memory barriers/fences + Hardware-level CPU
instructions(at runtime)
Compiler injects memory barriers that prevents the compiler from
reordering read and write operations outside atomic boundary
CPU instructions for atomicity:
x86/x64
LOCK XADD: For atomic additions. lock the cache line or memory
bus so other cores cannot access it during the operation.
ARM and RISC-V architectures
LDREX/STREX: Load-Exclusive and Store-Exclusive instructions.
Example: Set, load(read)
store = set the atomic. load = read it.
Default constructor leaves the value unspecified — always
initialize ({0} or ATOMIC_VAR_INIT).
#include <atomic>
#include <iostream>
int main() {
std::atomic<int> x{0}; // create, start at 0
x.store(5); // SET (default: seq_cst)
int v = x.load(); // READ (default: seq_cst)
std::cout << v << "\n"; // 5
x.fetch_add(1); // SET via add (default: seq_cst)
std::cout << x.load() << "\n"; // 6
}
Ordering?
int x = 0;
int y = 0;
void setup() {
x = 1; // Statement A
y = 2; // Statement B
}
To a human, Statement A comes before Statement B. However, compilers
(during optimization) may execute Statement B before Statement A(ie
shuffle) as long as the final result is the same. This is called
reordering.
Memory ordering for atomics
Memory order controls reordering of surrounding variables/control
around the atomic. It is a hint to the compiler and CPU about how to
order memory operations.
Default memory order: memory_order_seq_cst
| Order | Meaning | When to use |
|---|---|---|
memory_order_relaxed |
Guarantees atomicity only for that specific variable, not
surrrounding variables. memory_order_relaxed the atomic operation itself won't be torn(ie broken), but the compiler and CPU are completely free to reorder surrounding read and write operations in any way they want |
|
memory_order_release |
Acts as a barrier for preceding operations. Any memory writes (to atomic or regular variables) that happened before the release store are locked in place and cannot be reordered after it. | |
memory_order_acquire |
Acts as a barrier for subsequent operations. Any memory reads or writes that happen after the acquire load cannot be reordered before it. | |
memory_order_acq_rel |
Synchronized handshake between two threads. Weaker than sequential consistency because they don't enforce a global timeline |
|
memory_order_seq_cst (default memory order) |
Strictest ordering. Every thread in the program will agree on the exact chronological sequence in which atomic operations occurred, no matter how many CPU cores or caches are involved. drawback: To achieve this, the CPU must inject heavy hardware barriers (fences) that stall execution until caches are fully synchronized, making it safe but potentially slower. |
|
memory_order_consume (rarely used / discouraged) |
Weaker than acquire (data-dependent loads only). Compilers almost always promote it to acquire. | Do not use. Prefer acquire. |
Mixing 2 memory orders
Illegal combinations. Code will compile but will not work as expected.
// Typical pair (weaker than default, still correct for a flag + data)
data = 42;
ready.store(1, std::memory_order_release);
if (ready.load(std::memory_order_acquire) == 1)
use(data); // sees 42