Question bank — std - atomic — lock-free operations
Picture 1 — the race window (why plain x++ loses counts)
The figure below is a timeline: time flows left to right, one row per thread. Watch the red overlap band — that is the "race window" where both threads have loaded the same value before either stores.

Picture 2 — acquire/release as a one-way gate
This figure shows why memory order matters. Each thread has an ordered stack of operations; the arrows show which reorderings the compiler/CPU is allowed to do (green = allowed to slide, red = blocked by the gate).

True or false — justify
Marking a variable std::atomic<int> makes the statement a = a + 1; a single atomic step.
a + 1 is an atomic load, then a = ... is a separate atomic store — two operations with a gap between them where another thread can slip in (exactly the race window in Picture 1). Use a.fetch_add(1) or ++a for one indivisible read-modify-write.std::atomic always avoids taking a lock.
CMPXCHG16B). A std::atomic<BigStruct> of, say, 40 bytes has no single instruction that can swap it atomically, so the library hides an internal mutex. Only types where is_always_lock_free is true (or is_lock_free() returns true at runtime) are guaranteed lock-free.A lock-free algorithm guarantees every thread makes progress.
Under heavy contention a lock-free counter always beats a mutex-protected one.
memory_order_relaxed weakens the atomicity of an operation.
The default memory order for atomic operations is relaxed.
memory_order_seq_cst (sequentially consistent) — the safest and slowest. You must opt in to relaxed.compare_exchange_weak is buggy because it can fail even when the values match.
A release store on its own synchronizes two threads.
release store that publishes and a matching acquire load on another thread that reads the same value (the diagonal edge in Picture 2). One half alone creates no happens-before edge.memory_order_consume is just a cheaper synonym for acquire.
consume orders only operations that are data-dependent on the loaded value (e.g. dereferencing a pointer you just loaded), not all subsequent reads/writes like acquire does. It was meant for pointer-chasing on weakly-ordered CPUs, but it is so hard to specify correctly that current compilers promote it to acquire, so in practice it gives you no speedup today.Two threads doing counter++ on a plain int will always give the correct total, just slowly.
int increments can interleave in their load–add–store steps (Picture 1), so updates are silently lost — the final total is often less than expected, not just late. It is also formally undefined behaviour.is_lock_free() and is_always_lock_free mean the same thing.
is_always_lock_free is a compile-time constant guaranteed for that type on that platform; is_lock_free() is a runtime check on a specific object, which can matter for over-aligned or unusual instances.Spot the error
std::atomic<int> a{0};
a = a + 1; // "atomic increment"::: The comment lies. This is a load of a plus a separate store into a, with a race window between them (Picture 1). Replace with a.fetch_add(1) or ++a.
std::atomic<bool> ready{false};
int data = 0;
// producer
ready.store(true, std::memory_order_relaxed);
data = 42;::: Two bugs: the publish uses relaxed (no ordering, so the consumer may see ready==true before data==42), and data = 42 is written after the flag. The flag must be a release store placed after the data write.
Node* n = new Node{v, head.load()};
head.compare_exchange_weak(n->next, n); // no loop::: compare_exchange_weak can fail spuriously and fail on real contention, so a single call may silently not push. It must be wrapped in while(!head.compare_exchange_weak(n->next, n)){}.
std::atomic<int> a{0};
if (a.compare_exchange_strong(expected, 5)) { /* ... */ }
// expected was never initialized::: expected must be seeded with the value you believe a holds before the call. Uninitialized, the compare is meaningless; and on failure CAS overwrites expected with the current value of a, which you then must use.
std::atomic<int> x{0}, y{0};
// thread A
x.fetch_add(1, std::memory_order_relaxed);
y.fetch_add(1, std::memory_order_relaxed);
// thread B expects: if it sees y==1 then x==1::: With relaxed, there is no ordering between the two atomics across threads — B can observe y==1 while still seeing x==0. If B relies on that ordering, use release/acquire (or seq_cst).
Why questions
Why does the LOCK prefix (e.g. LOCK XADD) make an increment atomic without any software lock?
Why is a single CAS instruction enough to build any read-modify-write?
Why is relaxed the right choice for a pure statistics counter?
Why must the consumer's flag-read be an acquire load?
Why can a release store never let earlier writes "leak" after it?
Why was memory_order_consume intended to be faster than acquire on some hardware?
acquire needs a fence to order all following operations, but consume only needs to respect the natural data-dependency chain (the CPU already won't reorder a pointer dereference before the load that produced the pointer). That fence-free path is the intended win — undelivered in practice because compilers currently upgrade consume to acquire.Why does compare_exchange update expected on failure instead of leaving it alone?
expected, ready to recompute the desired value from.Why might a lock-free stack still suffer performance problems even though it never blocks?
head pointer bounce that cache line between cores — this cache-line ping-pong (a cousin of False Sharing) can dominate cost.Edge cases
What happens if two threads call push on the CAS-loop stack at the exact same moment?
compare_exchange succeeds; the other's fails because head changed, so n->next is refreshed to the new head and it retries. Both pushes eventually land — no update is lost.In a while(!head.compare_exchange_weak(n->next, n)){} push loop, what if the CPU makes compare_exchange_weak fail spuriously on the very first try even though nothing changed?
n->next is refreshed to the (unchanged) head and the next attempt succeeds. This is precisely why the weak form belongs inside a loop — the loop absorbs spurious failures for free.Does std::atomic<double> support fetch_add?
fetch_add was not provided for floating types, so you had to emulate it with a CAS loop.Can is_lock_free() return false for std::atomic<int> on a real platform?
What if expected in compare_exchange_strong happens to equal a but a was briefly changed to another value and back?
What ordering do you get if you call a.load() with no memory-order argument?
memory_order_seq_cst — the strongest, participating in a single global total order. Correct but potentially slower than acquire if that's all you needed.Is ++a (member operator) on std::atomic<int> the same as a = a + 1?
++a is a single atomic fetch_add; a = a + 1 is a separate atomic load and atomic store with a race window between them. Prefer the operator or fetch_add.Recall One-line self-test before you close the page
Name the one true difference between "atomic" and "ordered". ::: Atomic = the operation is indivisible (no torn/lost value); ordered = how other memory writes become visible around it. Relaxed keeps the first, drops the second.
See also: Mutex and Lock · std::thread and std::async · Memory Model (C++) · Compare-And-Swap · False Sharing