When two threads touch the same memory at the same time, the CPU does not guarantee that a read-modify-write (like x++) happens as one indivisible step. The compiler may reorder, the CPU may cache, and two x++ can interleave so you lose updates. std::atomic<T> says: "treat this variable specially — every operation on it is indivisible and visible to other threads in a controlled order." When the hardware can do this without taking a mutex , it is called lock-free .
Worked example The classic lost-update bug
int counter = 0 ; // plain int — NOT safe
// two threads each run:
for ( int i = 0 ; i < 100000 ; ++ i) counter ++ ;
// expected: 200000. actual: often LESS.
Why this step (why does it lose counts)? counter++ is really three machine steps:
load counter into a register
add 1
store register back
Thread A loads 5, Thread B loads 5, both add → both store 6. Two increments, one net effect. This three-step sequence is a read-modify-write (RMW), and it is the thing atomics make indivisible.
std::atomic<T> is a template wrapper whose operations (load, store, fetch_add, exchange, compare_exchange) are guaranteed to be atomic — observed by all threads as happening all-at-once, with no torn or interleaved states — and ordered according to a chosen memory order .
An operation is lock-free if it completes in a bounded number of steps without acquiring an OS mutex — typically via a single CPU instruction such as LOCK XADD or CMPXCHG. Check at runtime with a.is_lock_free() or at compile time with std::atomic<T>::is_always_lock_free.
Intuition One instruction, one cache line
Modern CPUs have a single instruction that does load-add-store as a unit , e.g. x86 LOCK XADD. The LOCK prefix tells the cache-coherency protocol: "hold exclusive ownership of this cache line for the whole instruction." No other core can sneak in between load and store. That is why no software lock is needed — the hardware provides the atomicity.
The fundamental primitive everything else is built from is Compare-And-Swap (CAS) :
Atomicity stops torn values. Memory order controls what other writes become visible around the atomic op.
Definition The orders you must know
==memory_order_relaxed== — atomic counter only, no ordering of surrounding memory. Cheapest.
==memory_order_acquire== (on a load) — no reads/writes after it can move before it.
==memory_order_release== (on a store) — no reads/writes before it can move after it.
==memory_order_seq_cst== (default) — single global total order; safest, slowest.
Intuition Acquire/Release = a one-way gate
A release store "publishes" everything you wrote before it. An acquire load "subscribes" — once it sees the released value, it also sees all those earlier writes. Together they form a synchronizes-with edge between two threads, like handing a sealed package: nothing leaks out before sealing, nothing is opened before receiving.
Worked example Producer / consumer flag
std ::atomic <bool> ready{ false };
int data = 0 ;
// producer
data = 42 ; // (1) ordinary write
ready. store ( true , std ::memory_order_release); // (2) publish
// consumer
while ( ! ready. load ( std ::memory_order_acquire)) {} // (3) wait
assert (data == 42 ); // (4) guaranteed!
Why step (2) must be release: it pins write (1) to happen before the flag is seen.
Why step (3) must be acquire: once it observes true, it is guaranteed to also see write (1). Without these orders, data could still read 0.
Worked example Atomic increment
std ::atomic <int> counter{ 0 };
// each thread:
for ( int i = 0 ; i < 100000 ; ++ i)
counter. fetch_add ( 1 , std ::memory_order_relaxed);
// result is EXACTLY 200000.
Why fetch_add? It is one atomic RMW → no lost updates.
Why relaxed is fine here: we only care about the count being correct , not about ordering it relative to other variables. The atomicity is preserved regardless of order; only cross-variable visibility is dropped. This is the cheapest correct choice — a key 80/20 optimization.
Worked example Lock-free stack push (CAS loop)
struct Node { int v; Node * next; };
std ::atomic < Node *> head{ nullptr };
void push ( int v ){
Node * n = new Node{v, head. load ()};
while ( ! head. compare_exchange_weak (n->next, n)) {}
}
Why CAS loop: if another thread pushed between our load and store, CAS fails, refreshes n->next to the new head, and retries. No mutex, no blocking.
Common mistake Steel-manned traps
1. "atomic makes the whole expression safe." Feels right because a is atomic. But a = a + 1; on an atomic<int> is two separate atomic ops (a load then a store) — not one RMW! Use a.fetch_add(1) or ++a (the member operator).
Fix: only the member functions / overloaded operators are atomic, not arbitrary expressions.
2. "lock-free means wait-free / faster always." Lock-free guarantees some thread progresses, not every thread, and a CAS loop under heavy contention can spin many times. Under high contention a mutex may even win.
Fix: measure; lock-free shines at low–medium contention.
3. "default relaxed is fine." The default is seq_cst, and relaxed drops ordering you may rely on.
Fix: use relaxed only when no other variable depends on this op's ordering.
4. compare_exchange_weak "is buggy" because it fails spuriously. It can fail even when values match (cheaper on some CPUs) — that's by design for use inside loops . Use _strong for single, non-looped checks.
Recall Feynman: explain to a 12-year-old
Imagine a shared piggy bank and two kids both adding a coin. If they both peek inside ("5 coins"), each thinks "now 6", and both write 6 on the label — one coin's count vanished. An atomic operation is like a magic hand that grabs, counts, and updates in one blink so no one can peek mid-way. Lock-free means we don't need to put a padlock on the bank (no waiting in line) — the magic is built into the bank itself. And memory order is the rule that says: if I leave a note "I'm done" on the box, anyone who reads that note also sees everything I packed inside.
Mnemonic Remember the toolkit
"LARS guards the CAStle" — L oad, A cquire, R elease, S tore are the moves; CAS is the gate that defends correctness. And R elaxed = R aw counter (atomicity only).
What does std::atomic<T> guarantee that a plain T does not? Operations are indivisible (atomic) and follow a chosen memory order, preventing torn reads and lost updates across threads.
Why is plain counter++ unsafe across threads? It is a load–add–store sequence; threads can interleave between the steps and lose updates.
Define "lock-free". An operation that completes in bounded steps without acquiring an OS mutex, usually via one CPU instruction (LOCK XADD / CMPXCHG).
What does compare_exchange_strong(expected, desired) do? Atomically: if value==expected set it to desired and return true; else load current value into expected and return false.
Why use a CAS loop for an arbitrary RMW? It writes only if no one changed the value since the read; on failure it refreshes and retries, giving lock-free progress.
Difference between compare_exchange_weak and _strong? weak may fail spuriously (cheaper, use inside loops); strong won't fail spuriously (use for single non-looped checks).
What pairing creates a synchronizes-with edge? A release store in one thread observed by an acquire load in another.
When is memory_order_relaxed appropriate? When you need atomicity only (e.g. a counter) and no other variable's visibility depends on this op's ordering.
Trap: is a = a + 1 atomic for atomic<int> a? No — it's a separate atomic load then atomic store; use a.fetch_add(1) or ++a.
How do you check lock-freedom at compile time? std::atomic<T>::is_always_lock_free (or is_lock_free() at runtime).
What is the default memory order if unspecified? memory_order_seq_cst (sequentially consistent, safest, slowest).
Mutex and Lock — the blocking alternative to lock-free atomics
Memory Model (C++) — happens-before, synchronizes-with
Cache Coherency MESI — why LOCK-prefixed instructions cost a cache line
std::thread and std::async — where these races arise
Compare-And-Swap — universal primitive for lock-free data structures
False Sharing — performance trap of atomics on the same cache line
three steps not indivisible
Two threads shared memory
Intuition Hinglish mein samjho
Dekho, jab do threads ek hi variable ko ek saath chhoote hain, tab problem hoti hai. counter++ dikhta toh simple hai par andar se woh teen steps hai — value load karo, +1 karo, wapas store karo. Agar do threads beech mein ghus jaaye, toh dono 5 load kar lete hain, dono 6 likh dete hain — ek increment gayab! Isko bolte hain lost update . std::atomic<T> yeh problem solve karta hai: woh operation ko indivisible bana deta hai, matlab beech mein koi ghus hi nahi sakta.
Lock-free ka matlab hai ki yeh kaam bina kisi mutex (taala) ke ho jaata hai — CPU ke paas ek hi instruction hota hai (jaise LOCK XADD) jo poora load-add-store ek jhatke mein kar deta hai. Isliye threads ko line mein khade hone ki zaroorat nahi, bahut fast. Sabse important tool hai CAS (compare-and-swap) : "agar value abhi bhi wahi hai jo maine padhi thi, toh nayi value daal do, warna fail batao aur main retry kar lunga." Isi CAS loop se hum lock-free stack, queue sab bana lete hain.
Doosri cheez hai memory order . Atomicity sirf torn value rokti hai, par doosre variables ki visibility control karne ke liye acquire/release use karte hain. Producer release se "publish" karta hai, consumer acquire se "subscribe" — yeh ek gate banata hai jisse pehle ke saare writes guarantee ho jaate hain. Simple counter ke liye relaxed enough hai (sabse sasta), par jab doosre data ki ordering important ho tab acquire/release lagao. Default seq_cst hota hai — safe par slow.
Ek galti se bacho: a = a + 1 atomic variable par bhi safe nahi hai, kyunki woh do alag atomic ops hai (load phir store). Hamesha a.fetch_add(1) ya ++a member operator use karo. Yeh chhoti baat exam aur real code dono mein bahut bar galat hoti hai.