Worked examples — Synchronization primitives (locks, barriers)
This page is the practice arena for the parent topic. The parent built the machinery: locks, Test-and-Set / Compare-and-Swap, spin-lock cost formulas, and reusable barriers. Here we run those machines through every case they can face, so you never meet a scenario cold.
Before we start, a one-line refresher on the symbols we will reuse, so nothing is used before it's earned:
Recall Symbols we inherit from the parent (click to expand)
- ::: total number of threads contending for a lock or arriving at a barrier.
- ::: the critical-section hold time — how long one thread keeps the lock (seconds).
- ::: expected wait for one arrival landing at a random moment behind a single holder.
- ::: mean wait averaged over the positions of an -deep FIFO queue.
- ::: time for one spin-loop iteration (one failed Test-and-Set attempt).
- ::: energy cost of executing one instruction.
- TAS ::: Test-and-Set — atomically reads a flag and sets it to 1, returning the old value.
- CAS ::: Compare-and-Swap — writes
newonly if the location still equalsexpected.
The scenario matrix
Every problem this topic throws at you falls into one of these cells. Each cell is a distinct kind of trap. The examples below are labelled with the cell they hit — together they cover the whole grid.
| # | Cell class | What varies | Example |
|---|---|---|---|
| A | Race baseline — no lock | interleaving order | Ex 1 |
| B | Lock cost, low contention | small | Ex 2 |
| C | Lock cost, high contention | large , queue wait | Ex 3 |
| D | Degenerate: | zero contention | Ex 4 |
| E | Spin energy waste | ratio | Ex 5 |
| F | CAS success vs failure | expected matches or not | Ex 6 |
| G | Barrier reuse race | generation counter | Ex 7 |
| H | Barrier limiting case: one slow thread | straggler dominates | Ex 8 |
| I | Real-world word problem | translate to formula | Ex 9 |
| J | Exam twist: deadlock ordering | two locks, two threads | Ex 10 |
Cell A — the race with no lock
Forecast: guess before reading — is the final value always 2? Always 1? Or does it depend?
- Break each thread into 3 atomic steps. Thread 1 = (Load, Add, Store); Thread 2 = .
Why this step? The parent showed the hardware doesn't run
counter = counter+1as one action — it's three. Correctness lives at the step level. - A correct result (2) needs one thread's store to happen before the other thread's load. i.e. before , or before . Why this step? If a thread reads after the other has stored, it sees the updated value and adds on top of it.
- A lost update (final = 1) happens whenever both loads precede the FIRST store, e.g. . Why this step? Both read 0, both compute 1, both write 1 → one increment vanished.
- Count directly. Enumerate the 20 order-preserving interleavings and simulate each. The safe condition (a store precedes the other thread's load) holds for 16 interleavings; the remaining 4 are lossy (final = 1). Why this step? Rather than argue by symmetry (which is easy to miscount), we simulate every ordering — the mechanical check the =VERIFY= block reproduces.
Verify: The number of order-preserving interleavings of two 3-step sequences is . Direct simulation gives 16 safe (final value 2) and 4 lossy (final value 1). So the answer is not always 2 — it goes wrong in 4 of 20 orderings (20% of the time).
Cells B, C, D — spin-lock waiting time
Forecast: 40? 20? 80?
- Recognise this is "one arrival behind one holder". Use . Why this step? The parent's formula: a uniformly-random arrival waits on average for half the remaining hold interval.
- Substitute: .
Verify: Units: . ✓ Half of 40 is 20. ✓
Forecast: will the mean equal again, or scale with ?
- The last thread waits for all 4 ahead of it. . Why this step? In FIFO, position waits for the full critical sections in front.
- Average over positions : . Why this step? The mean of the arithmetic run is . This is a different quantity from Ex 2's — see the parent's warning.
Verify: , and (mean is half the max of a uniform run). ✓ Contrast with Ex 2's : raising from 2 to 5 raised mean wait . This is exactly why blocking locks matter (see Deadlock and livelock and Thread scheduling).
The figure below is this example drawn out. Each blue bar is the wait suffered by a thread sitting at that queue position: the holder (position 0) waits nothing, and every step further back adds one more full critical section (), so the bars climb . The red bar is the worst-placed (last) thread waiting the full . The yellow dashed line is the mean — and notice it lands exactly halfway up the tallest bar, which is the geometric reason the queue average equals : the mean of an evenly-spaced run is always half its maximum. Reading the bars left-to-right lets you see the linear growth that punishes high contention.

Forecast: zero wait? or still some cost?
- Contention wait: with there is no other holder, so . Why this step? Plugging into the queue formula gives 0 — the degenerate boundary sanity-checks the formula.
- But the lock is not free: the single TAS instruction still executes (one atomic read-modify-write) plus a store on release. Why this step? Even uncontended, an atomic op forces a coherence transaction (get the line in exclusive state). This is the "uncontended lock overhead."
Verify: . ✓ Zero waiting, non-zero instruction cost — both statements consistent.
Cell E — energy wasted by spinning
Forecast: milli-joules? nano-joules? how does it scale with ?
- Count spin iterations per waiting thread: iterations during one hold. Why this step? A thread spinning for the whole hold burns one instruction every .
- Multiply by the waiters: total iterations . Why this step? Each of the 4 waiters spins; the holder does not spin.
- Multiply by energy per instruction: .
Verify: . ✓ Units: . ✓ Grows linearly with — the parent's motivation for blocking locks over spinning.
Cell F — CAS success vs failure
Forecast: will B's swap succeed?
- A's call: does actual (7) equal expected (7)? Yes → write
new=9. CAS returns the old actual = 7. Memory is now 9. Why this step? CAS writes only on a match and always returns what it saw. A saw 7, so it swapped. - B's call: does actual (9) equal expected (7)? No → no write. CAS returns actual = 9. Memory stays 9.
Why this step? B's
expectedis stale — the world changed under it. This is the CAS failure case; a real lock-free loop would re-read and retry withexpected=9.
Verify: A returns 7, memory→9 (match branch). B returns 9, memory stays 9 (no-match branch). Both cells of the CAS matrix (match / no-match) covered. ✓ See Atomic operations for how retry loops build on this.
Cells G, H — barriers
Before the barrier examples, two symbols we will use inside the trace:
Recall Barrier symbols for Ex 7 (click to expand)
threshold::: the barrier trip count — the total number of threads that must arrive before the gate opens. Here .my_gen::: a per-thread local copy of the barrier's currentgeneration, recorded the moment a thread arrives, so it can tell its episode apart from the next one.generation::: the barrier's shared phase counter; the last-in thread increments it to open the gate for the current episode.count::: how many threads have arrived in the current generation (reset to 0 when the gate opens).
Forecast: what is generation after two full phases?
- Phase 0 start:
generation=0,count=0. Threads arrive: each recordsmy_gen=0, thencountgoes . Why this step? Eachbarrier_waitincrementscountunder the lock and recordsmy_gen= the generation it belongs to. - 3rd thread trips the threshold:
count == threshold (3)→ setgeneration=1, resetcount=0, broadcast. Why this step? The last-in thread opens the gate and advances the generation. Waiters wake and check: theirmy_gen(0) != generation(1)→ they proceed. - Phase 1: a fast thread loops back, records
my_gen=1,countgoes ; last-in setsgeneration=2, resetscount=0. Why this step? Because phase-1 threads recordedmy_gen=1, a stale phase-0 wakeup can't accidentally satisfy them — the generation check separates episodes. This is the exact race the parent's "reset count = 0 alone" version failed.
Verify: After two phases, generation = 2 and count = 0. Each phase advanced generation by exactly 1 and left count at 0 ready for reuse. ✓
Forecast: is the release time the average arrival, or the max?
- A barrier releases only when the LAST thread arrives. Release time . Why this step? The definition: no thread proceeds until all have arrived — the slowest dictates.
- Idle time = sum over the early threads of (release − their arrival). Three threads each wait : total . Why this step? Each early thread sits blocked from its arrival until release.
Verify: Release (max, not the mean of 25). Idle . ✓ Lesson: barriers are only as fast as the slowest thread — the motivation behind load-balanced Parallel algorithms.
The figure below is this example laid on a timeline, one row per thread. The three green dots are the punctual arrivals at ; the red dot is the straggler landing at . The yellow bars are the idle stretches — each punctual thread is blocked from the moment it arrives until the blue dashed release line at , so each yellow bar is long. The pedagogical punch: add the three equal yellow bars () and you read off the total idle by eye, and you see that it is the single red straggler — not the average arrival — that sets the release line. That is the whole point of "a barrier runs at the speed of its slowest thread."

Cell I — real-world word problem
Forecast: microseconds? milliseconds? which formula — or the queue one?
- Identify the cell: 50 contenders queueing = high-contention FIFO (cell C machinery). Why this step? This is Ex 3 dressed as a word problem — pick , not the single-arrival .
- Average wait: . Why this step? With 50 requests in a FIFO line, a typical request sits at a middle position; the mean over all positions is — the same queue formula proven in Ex 3, now with .
- Worst request: . Why this step? The unluckiest request is dead last, waiting for all critical sections ahead of it — the top of the arithmetic run, exactly twice the mean.
Verify: average, and worst case . ✓ Units in . ✓ Real lesson: at high , a single global lock serialises everything — why real servers shard the account or use per-account locks (a consistency vs throughput trade-off).
Cell J — exam twist: deadlock by lock ordering
Forecast: can this always complete, or is there a hang?
- Find the deadly interleaving: A gets L1, B gets L2. Now A waits for L2 (held by B), B waits for L1 (held by A). Why this step? Each holds one lock and blocks forever on the other — the classic circular-wait cycle.
- Confirm all four Coffman conditions hold: mutual exclusion (locks), hold-and-wait (A holds L1, wants L2), no preemption (can't steal a held lock), circular wait (A→L2→B→L1→A). Why this step? Deadlock needs all four; here they all appear, so a hang is possible (not guaranteed on every run — only on the bad interleaving).
- The fix — global lock ordering: make every thread acquire locks in the same order, e.g. always L1 before L2.
Why this step? If both A and B do
acquire(L1); acquire(L2), no circular wait can form — whoever gets L1 first will get L2 next. This breaks the cycle condition.
Verify: With ordered acquisition, the "hold-and-wait cycle" is impossible because both request L1 first. ✓ Deep-dive continues in Deadlock and livelock.
Recall Self-test (cover the right side)
Final value range with no lock on two increments from 0 ::: either 1 (lost update, in 4 of 20 orderings) or 2 (safe, in 16 of 20)
for :::
Wait for the last of threads :::
Barrier release time = ? of arrival times ::: the max (slowest thread)
CAS with stale expected ::: no write, returns current value, caller retries
Fix for two-lock deadlock ::: global lock ordering (same order everywhere)