Exercises — Atomic operations and CAS
Before we start, one shared vocabulary reminder so no symbol is used unearned:
Level 1 — Recognition
Exercise 1.1
Which of these operations is atomic on typical hardware, and which is a race-prone read-modify-write?
(a) x = 5; (single aligned 32-bit store)
(b) counter++;
(c) CAS(&p, old, new)
(d) total += array[i];
Recall Solution
Atomic: (a) and (c). Not atomic: (b) and (d).
- (a) A single aligned store of a machine word is atomic — no other thread can see it half-written.
- (b)
counter++compiles to load → add → store: three separate steps with a race window between them. - (c) CAS is a single hardware primitive, atomic by definition.
- (d)
+=is also load → add → store, same race window as (b). Count of atomic operations: 2.
Exercise 1.2
CAS is called . If *addr = 7, what does CAS(addr, 4, 9) do to memory and what does it return?
Recall Solution
The stored value is but . Since , the compare fails: memory stays and CAS returns false. Return = false, memory unchanged (still 7).
Level 2 — Application
Exercise 2.1
Trace atomic_increment on a counter starting at , where CAS succeeds on the second attempt (another thread bumped the counter to between your read and your CAS). List each old_val, new_val, and CAS result. What is the final counter value?
Recall Solution
old_val = *counter // read 10
new_val = old_val + 1 // 11
CAS(counter, 10, 11) // FAILS: counter is now 11, not 10
old_val = *counter // re-read 11
new_val = old_val + 1 // 12
CAS(counter, 11, 12) // SUCCEEDS: counter was 11
- Attempt 1:
old_val=10,new_val=11, CAS → false - Attempt 2:
old_val=11,new_val=12, CAS → true - Final counter = 12. (The interfering thread's increment made it 11, ours made it 12 — no update lost.)
Exercise 2.2
Using fetch_and_add(addr, delta) which returns the value before the add: if *addr = 100 and two threads run fetch_and_add(addr, 5) and fetch_and_add(addr, 3), what is the final value of *addr, and what two return values are possible (list both orderings)?
Recall Solution
Fetch-and-add is atomic, so the two adds compose regardless of order. Final value = . Return values depend on order:
- If +5 runs first: it returns , the +3 returns .
- If +3 runs first: it returns , the +5 returns . Exactly one thread always sees ; the other sees the partially-updated value. Final = 108 either way.
Level 3 — Analysis
Exercise 3.1
A spinlock uses test_and_set, which atomically writes and returns the old value. The lock is "free" when the word is . Write the acquire loop and explain: what old value means "I got the lock"? See the figure.

Recall Solution
void acquire(int* lock) {
while (test_and_set(lock) == 1) {
// spin: old value was 1 => lock was already held
}
// old value was 0 => it was free, and we just set it to 1: we own it
}You got the lock when test_and_set returns . Returning means the word was free and you atomically flipped it to in the same indivisible step. Returning means it was already held by someone; you keep spinning. See Spinlocks and Mutexes.
Exercise 3.2
Below is a lock-free pop trace. Identify the exact line where the ABA problem strikes and why CAS wrongly succeeds.
Initial: stack_top -> A -> B -> NULL
T1: old_top = A
T1: new_top = A.next = B
[T1 preempted]
T2: pop A (stack_top = B)
T2: pop B (stack_top = NULL)
T2: push A (stack_top = A, reusing freed node A)
T1: CAS(&stack_top, A, B) <-- ???
Recall Solution
The final CAS wrongly succeeds. T1's expected = A and stack_top is once again A, so the pointer comparison matches — but the structure changed underneath. T1 sets stack_top = B, yet was already popped and freed by T2. Result: stack_top points to freed memory → corruption.
The root cause: CAS compares the pointer value, not the object identity. The pointer returned to (the "A → B → A" sequence), so CAS cannot tell that everything moved. Fix in ABA Problem Solutions and Exercise 4.1.
Level 4 — Synthesis
Exercise 4.1
Fix Exercise 3.2 with a version counter (tagged pointer). The stacked value is a pair and double-width CAS must match both fields. Suppose T1 read . During the A→B→A recycling, T2 performed 3 modifications (pop, pop, push). What version does stack_top hold when T1 finally runs CAS, and does T1's CAS succeed?
Recall Solution
Each modification increments the version by :
- start:
- after pop A:
- after pop B:
- after push A:
T1's CAS uses expected = ⟨A, 7⟩. Current is . The pointer matches () but , so double-width CAS fails — exactly what we want. Final version = 10; T1's CAS fails and it safely retries. The ABA is defeated.
Exercise 4.2
Derive swap(addr, new) (unconditionally store new, return the old value) using only CAS in a loop. Prove it always terminates in the absence of contention with exactly one CAS.
Recall Solution
int swap(int* addr, int new) {
int old_val;
do {
old_val = *addr; // read current
} while (!CAS(addr, old_val, new));// install new only if unchanged
return old_val;
}No contention: we read old_val, and nothing modifies *addr before CAS, so *addr == old_val still holds. CAS succeeds on the first attempt → exactly one CAS, then we return old_val. With contention, each failure means someone else wrote, so we re-read and retry; every retry makes progress by some thread (lock-freedom). See Lock-Free Data Structures.
Level 5 — Mastery
Exercise 5.1
On an ARM/RISC-V machine you have LL/SC instead of CAS. LL(addr) loads and sets a reservation; SC(addr, val) stores and returns on success, if the reservation was broken by any intervening write. Explain in one line why LL/SC is immune to ABA in the A→B→A scenario of Exercise 3.2, without any version counter. Then implement CAS on top of LL/SC.
Recall Solution
Immunity: SC fails if any write touched the address after the LL — even a write that restores the same value. So the A→B→A sequence breaks the reservation and SC returns , unlike CAS which only compares final bits. (Caveat: hardware may lose reservations spuriously, so LL/SC always lives in a retry loop.)
bool CAS(int* addr, int expected, int new) {
int cur = LL(addr); // load + reserve
if (cur != expected)
return false; // value differs, give up
return SC(addr, new) == 1; // store only if reservation intact
}If SC returns (reservation broken), CAS returns false, matching CAS semantics. See Memory Barriers for the ordering fences usually paired with this.
Exercise 5.2
A counter is incremented by threads, each doing successful atomic increments via a CAS loop. Give the final counter value and a lower bound on the minimum total number of CAS executions (successful + failed) across all threads. Compute both for , .
Recall Solution
Final value: every increment lands exactly once (CAS guards against lost updates), so final . Minimum total CAS executions: in the best case there is zero contention — every CAS succeeds first try — so each of the successful increments costs exactly one CAS. Minimum . (Failures only add to this; the count can be much higher under contention, but never lower than .) Answers: final = 4000, minimum total CAS = 4000.
Recall Self-test cloze
An atomic op executes completely and indivisibly, so no thread sees a partially-completed state.
CAS returns true only when *addr equals expected.
The ABA problem fools CAS because it compares the pointer value not the object identity.
Version counters defeat ABA by forcing CAS to match both the pointer and the version.
LL/SC is ABA-immune because SC fails on any intervening write, not just a value mismatch.