Worked examples — Atomic operations and CAS
Before we start, one promise: every symbol is earned. Let us fix the vocabulary with a picture.

Look at the figure: the mailbox holds a value, you hold two cards — an expected card and a new card. CAS compares your expected card to what is inside. Match → it swaps in your new card. Mismatch → it slaps your hand away and tells you the real value so you can try again.
We will write memory contents using , read aloud as “the value stored at that address” (the star means “look inside the box”). So means the box currently holds 10.
The scenario matrix
Every CAS story falls into one of these cells. The examples below are labelled with the cell they hit, and together they cover all of them.
| Cell | Scenario class | What is being tested |
|---|---|---|
| A | CAS succeeds first try | Guess matches → clean swap |
| B | CAS fails once, retries, succeeds | Contention / the retry loop |
| C | CAS must legitimately fail forever unless value changes | Conditional update (only-if) |
| D | Building fetch-and-add from CAS | Derived atomic + return-old semantics |
| E | Zero / degenerate input | , empty stack, single-element |
| F | Limiting behaviour | High contention → livelock, throughput |
| G | ABA trap (A→B→A) | Pointer same, structure changed |
| H | ABA fixed with version counter | Tagged pointer / double-width CAS |
| I | Real-world word problem | Ticket counter, seat booking |
| J | Exam twist | Weak vs strong CAS, spurious failure |
Example A — CAS succeeds on the first try
Forecast: guess return value and final box contents before reading on.
- Read the current value. . Why this step? CAS internally compares against ; we must know to reason about it.
- Compare to . Is equal to ? Yes. Why this step? This equality is the entire decision CAS makes.
- Swap in . Since they match, and CAS returns true. Why this step? A matching guess is CAS's licence to write.
Verify: final , return true. Sanity check: exactly one thread, no interference, so of course the guess held.
Example B — CAS fails once, then retries and wins
void atomic_increment(int* counter) {
int old_val, new_val;
do {
old_val = *counter;
new_val = old_val + 1;
} while (!CAS(counter, old_val, new_val));
}Forecast: how many times does Thread 1 call CAS, and what is the final value after both threads finish?
- Attempt 1 — read. Thread 1 reads , computes . Why this step? We must snapshot the value we intend to modify.
- Interleave. Thread 2 runs first, does → success. Now . Why this step? This is the race we are stress-testing — the box changed under Thread 1's feet.
- Attempt 1 — CAS. Thread 1 calls . But . Returns false. Loop repeats. Why this step? Thread 1's guess (5) is now stale; strong CAS's false here means a genuine mismatch — it correctly refuses to clobber Thread 2's work.
- Attempt 2 — read. Thread 1 re-reads , computes . Why this step? Fresh read = fresh, correct guess.
- Attempt 2 — CAS. → success. .

Verify: two increments starting from 5 must give . Final value 7 ✓ — no update was lost, which is the whole point. Thread 1 called CAS twice. Contrast with the lost-update bug where both threads would have written 6.
Example C — CAS that must fail (only-if update)
Forecast: true or false? Does the box change?
- Compare. vs . Not equal. Why this step? The programmer wants the write to happen only from state 0; a mismatch means the precondition is not met.
- No write. CAS leaves untouched and returns false. Why this step? This is CAS as a guarded write — the guard failed, so nothing happens. This is exactly how test-and-set spinlocks avoid stomping a held lock.
Verify: box unchanged at 1, return false. Sanity: with strong CAS a false definitively means "condition not met" (not a spurious lie). The caller decides whether to retry, wait, or abort.
Example D — Deriving Fetch-And-Add, and the return-old subtlety
int fetch_and_add(int* addr, int delta) {
int old_val, new_val;
do {
old_val = *addr;
new_val = old_val + delta;
} while (!CAS(addr, old_val, new_val));
return old_val; // OLD value, not new!
}Forecast: returned value and final box value — they are different; predict both.
- Read. . Why this step? We need the pre-increment value both to compute the sum and to return it.
- Compute. . Why this step? Do the arithmetic outside the atomic step so the atomic part stays a single cheap compare-swap.
- CAS. → success (no contention). .
- Return. Return . Why this step? Fetch-and-add's contract is "give me the value before my add." That is what makes it useful for handing out unique indices — each caller gets a distinct old value.
Verify: returns 10, box becomes 13. Check: ✓. This is the invariant .
Example E — Degenerate inputs
Forecast: does E1 change anything? Does E2 dereference NULL and crash?
E1 — zero delta:
- Compute. . Why this step? is the identity for addition — new equals old.
- CAS. succeeds (7 equals 7), writes 7 over 7 (a no-op write). Why this step? CAS still succeeds; it just writes the same value. Return is .
Verify E1: returns 7, box stays 7. A zero-delta fetch-and-add is a valid atomic read — a legitimate idiom.
E2 — pop from empty stack:
Node* pop() {
Node* old_top;
Node* new_top;
do {
old_top = stack_top;
if (old_top == NULL) return NULL; // guard FIRST
new_top = old_top->next;
} while (!CAS(&stack_top, old_top, new_top));
return old_top;
}- Read top. . Why this step? Snapshot before touching anything.
- Guard. Since is NULL, return NULL immediately — never dereference NULL.
Why this step? Skipping the guard would do
NULL->next, a crash. Every real lock-free structure needs this degenerate check.
Verify E2: returns NULL, no dereference, no CAS attempted. Correct empty-stack behaviour.
Example F — Limiting behaviour: high contention & livelock
Forecast: does the CAS loop deadlock, livelock, or just slow down?
- No deadlock. At every instant at least one thread's CAS succeeds (whoever's guess matches the current value). Why this step? CAS is lock-free: system-wide progress is guaranteed because a failing CAS means someone else made progress.
- But possible starvation. A specific unlucky thread can keep losing races and retry indefinitely — that is livelock for that thread, not the system. Why this step? Lock-freedom guarantees some thread progresses, not every thread. Fairness needs extra work (see Lock-Free Data Structures).
- Throughput saturates. Each successful CAS invalidates the cache line on all other cores (see Cache Coherence Protocols), so more threads → more coherence traffic → the line "ping-pongs." Throughput plateaus and can even drop. Why this step? The limiting cost is cache-line bouncing, not CPU arithmetic.
Verify: limiting case reasoning — with increments total the final value is always regardless of interleaving; correctness never breaks, only speed. E.g. start , threads each +1 once → final .
Example G — The ABA trap fires
Forecast: Thread 1's CAS — does it succeed? Is that success correct?
- T1 reads. , plans . Then T1 is preempted. Why this step? T1's guess "top is A, next is B" is now frozen in time.
- T2 pops A. . Node is freed.
- T2 pops B. . Node is freed.
- T2 pushes A back (memory allocator reused the freed block). . Why this step? The pointer value returned, but is now NULL, not .
- T1 resumes CAS. . Current top is , so strong CAS succeeds — and sets . Why this step? Disaster: was freed in step 3. The stack now points into reclaimed memory. Note this is not a spurious failure issue — CAS did exactly what strong CAS promises; the flaw is that value-equality is not identity-equality.

Verify: CAS returned true (pointer matched) yet the result is corrupt. The bug is that CAS compares the bits of the pointer, not the identity of the object. This is the ABA problem.
Example H — ABA fixed with a version counter
Forecast: does T1's CAS succeed this time?
- T1 reads. , plans . Preempted. Why this step? T1 captures both the pointer and the version at read time.
- T2 pops A → . Pops B → . Pushes A → . Why this step? Three successful modifications bumped the version three times: .
- T1 resumes CAS. . Current is . Pointer matches, but → CAS fails. Why this step? The version acts as a "generation stamp." Even though the pointer wrecked its way back to , the world moved on and the stamp proves it.
- T1 retries with a fresh read of — safe now.
Verify: CAS returns false, corruption avoided. Sanity check on versions: 3 modifications ⇒ version increased by exactly 3, . This needs double-width CAS (128-bit on x86: CMPXCHG16B). See Memory Barriers for why the version write and pointer write must be one indivisible unit.
Example I — Real-world word problem: ticket dispenser
Forecast: the three seat numbers and the final counter.
- Request 1.
fetch_and_add(counter,1)returns the old value ; counter → . Why this step? Fetch-and-add returns the pre-increment value — that is the assigned seat, guaranteeing uniqueness. - Request 2.
fetch_and_add(counter,1)returns ; counter → . Why this step? Whatever the interleaving, atomicity means no two requests can read the same old value — each caller sees a distinct snapshot, so no seat is handed out twice. - Request 3.
fetch_and_add(counter,1)returns ; counter → . Why this step? The third caller likewise gets its own unique pre-increment value, completing the batch of three distinct seats.
Verify: seats assigned — all distinct ✓. Final counter ✓ (start number of requests). No double-booking, no locks. This is why fetch-and-add is the go-to primitive for ID/seat generation.
Example J — Exam twist: weak vs strong CAS (spurious failure)
Forecast: bug, or legal behaviour?
- Recognise the mechanism. On ARM/RISC-V, CAS is built from Load-Link / Store-Conditional. The SC can fail because the reservation was broken by an unrelated event (an interrupt, a context switch, another access to the same cache line). Why this step? The parent note's LL/SC section is the key: SC "fails if the reservation is broken," and a reservation can break for reasons having nothing to do with the value — this is exactly the spurious failure defined at the top of the page.
- Weak CAS forwards that spurious failure. may return false even when . This is legal and documented behaviour, not a hardware fault. Why this step? Exposing spurious failure lets the compiler emit a single LL/SC pair (fast) instead of an internal retry loop.
- Strong CAS hides it. retries internally so it only returns false on a genuine value mismatch — but costs an inner loop. This is why every earlier example on this page assumed strong CAS: their "false always means real mismatch" reasoning only holds for the strong variant. Why this step? Choose weak inside a loop you already retry (increment, push); choose strong for a one-shot conditional update (Example C).
Verify: returning false with unchanged is correct for weak CAS — not a bug. The programmer's outer loop simply retries: on the next iteration still reads , the guess still matches, and (assuming no further spurious break) the increment lands . Same final result as strong CAS, e.g. box ends at . This trade-off is tied to memory ordering semantics.
Recall Self-test
Strong vs weak: which one can return false while the value still matches? ::: Weak CAS — a spurious failure from a broken LL/SC reservation. Strong CAS's false always means a real mismatch.
Example B: how many times did Thread 1 call CAS? ::: Twice — one failure (stale guess 5), one success (guess 6).
Cell C: is a CAS that returns false an error? ::: No — with strong CAS it is a legitimate "precondition not met"; the caller decides what to do.
Cell D: does fetch-and-add return the old or new value? ::: The old (pre-increment) value.
Cell D: is the prose the same as the code delta? ::: Yes — identical quantity, just different alphabets (Greek vs C).
Cell G: why did the buggy CAS succeed despite corruption? ::: CAS compares pointer bits, not object identity — the recycled pointer matched.
Cell H: after 3 modifications, version went 7 → ? ::: 10 (one increment per successful modification).
Cell I: three fetch-and-adds from 100 give which seats and final counter? ::: Seats 100, 101, 102; counter 103.
Cell J: can weak CAS return false when values match? ::: Yes — spurious failure; legal, handled by the retry loop.