5.2.26 · D3C++ Programming

Worked examples — std - atomic — lock-free operations

3,363 words15 min readBack to topic

The scenario matrix

Before any code, let us list every kind of situation atomics can throw at you. Think of it as a checklist — if we work one example per row, you will never meet an unseen case.

Cell Scenario class The core question it tests
A Simple RMW, no cross-variable dependence Does fetch_add fix lost updates? Is relaxed enough?
B RMW that is not a built-in op (custom function) Can we still be atomic via a CAS retry loop?
C High-contention limiting case What happens as the number of retries grows? Does progress still hold?
D Cross-variable visibility (publish/subscribe) Why do we need acquire/release, not relaxed?
E The a = a + 1 degenerate trap Why is an "obvious" atomic line not one atomic op?
F Zero / boundary inputs (empty pop, single winner) What if the structure is empty, or two threads both try at once?
G Real-world word problem Ticket-booking seat allocation — one seat, many buyers
H Exam-style twist weak spurious failure & the "ABA" surprise

We now fill all eight cells.


Cell A — simple atomic counter


Cell B — custom RMW via a CAS loop

Below is the same loop drawn as a flow: read → compute → CAS commits, with the single-thread "no contention → immediate win" path highlighted in amber. Follow the white arrows left-to-right, then down into the success box.

Figure — std - atomic — lock-free operations

Cell C — high contention limiting behaviour


Cell D — cross-variable visibility (acquire/release)

The figure below draws this as a one-way gate between two threads: the amber synchronizes-with arrow is the only channel, and everything the producer wrote above its release is forced across it before the consumer's acquire lets execution proceed. Read the producer column top-down, cross the amber arrow, then read the consumer column.

Figure — std - atomic — lock-free operations

Cell E — the a = a + 1 degenerate trap


Cell F — zero / boundary inputs (empty stack, single winner)


Cell G — real-world word problem: one seat, many buyers


Cell H — exam twist: spurious failure & the ABA surprise


Recall Quick self-test: name the cell

A fetch_add(1, relaxed) counter is correct because... ::: each op is one indivisible RMW; relaxed keeps atomicity, only cross-variable ordering is dropped (Cell A). Worst-case CAS failures for colliding threads ::: , quadratic — lock-free is not wait-free (Cell C). Why is a = a + 1 on an atomic still buggy? ::: it is load-then-store (two ops with a gap), not one RMW; use fetch_add/++a (Cell E). In a lock-free push, why must the successful CAS be a release-store? ::: so the new node's fields (v, next) are published before the head pointer becomes visible; else a popper reads uninitialized data (Cell F). One seat, four buyers, how many succeed? ::: exactly one; CAS gives a single winner (Cell G). Does a spurious weak failure change the final value? ::: no — only costs extra loop iterations; correctness is untouched (Cell H).


Parent: Hinglish version · Related: Compare-And-Swap · Memory Model (C++) · Cache Coherency MESI · std::thread and std::async · False Sharing · Mutex and Lock

Flashcards

Worst-case number of failed CAS attempts for n threads colliding on one loop?
The arithmetic series 0+1+...+(n-1) = n(n-1)/2, showing lock-free is not wait-free.
In the ticket-booking (one seat, four buyers) example, why use compare_exchange_strong not weak?
It is a single non-looped check, so we must not tolerate spurious failures; strong never fails spuriously.
What is the ABA problem?
CAS compares values not history; if a value goes A to B back to A, CAS wrongly succeeds. Fix with version tags or hazard pointers.
Why must the successful CAS in a lock-free stack push use memory_order_release?
To publish the new node's initialized fields before the head pointer is visible, so an acquiring popper never reads uninitialized data.