Exercises — Synchronization primitives (locks, barriers)
Before we start, one shared vocabulary refresher (so no symbol is used before it is earned):
- = number of threads competing.
- = time (in seconds, or in "instruction-count") that one thread holds the lock — the length of the critical section.
- = time for one spin iteration (one failed test-and-set + loop).
- A spin lock keeps checking; a blocking lock goes to sleep.
- A condition variable (written
cond_t cv, and abbreviated CV) is a "waiting room": a thread canwaiton it to sleep until another threadsignals orbroadcasts to wake it.
Level 1 — Recognition
L1.1 — Which primitive?
You have two threads, each doing counter = counter + 1 a million times on a shared counter. You want the final value to be exactly . Name the ONE primitive that fixes this, and say why a plain bool flag does not.
Recall Solution
The fix is a lock (mutex) built on a hardware atomic such as Test-and-Set or Compare-and-Swap.
A plain bool flag fails because "check then set" is two instructions — between the check and the set, the other thread slips in. Only an atomic read-modify-write reads the old value AND writes the new value in one indivisible step, so exactly one thread can win. See Atomic operations.
L1.2 — Barrier vs lock
For each scenario, say whether you want a lock or a barrier:
- (a) Ten threads each add their partial sum into one shared total.
- (b) A simulation runs in phases; every thread must finish phase 1 before ANY thread starts phase 2.
Recall Solution
- (a) Lock — protects one shared variable from simultaneous writes (mutual exclusion).
- (b) Barrier — a meeting point where all threads wait until everyone has arrived, then all proceed together. The tell: "one resource, many writers" → lock. "one wall everyone waits at" → barrier.
Level 2 — Application
L2.1 — Trace Test-and-Set
TestAndSet(&flag) atomically returns the OLD value of flag and sets it to 1. Lock is free ⇒ flag = 0. Three threads T1, T2, T3 call acquire and their atomic TAS instructions happen in the order T2, T1, T3. Who enters the critical section, and what does each TAS return?
Recall Solution
acquire spins while TAS returns 1.
- T2 runs first: reads old value
0, sets flag to1, returns0→0 != 1, so T2 enters. - T1: reads old value
1, returns1→ spins. - T3: reads old value
1, returns1→ spins. Exactly one winner (T2). The returned values are[0, 1, 1]in the order (T2, T1, T3).
L2.2 — Per-arrival spin wait
A lock is held for . A thread arrives at a uniformly random instant during the current holder's critical section. What is its expected wait for the current holder to release?
Recall Solution
Level 3 — Analysis
L3.1 — FIFO queue average wait
Now threads arrive at nearly the same time and queue behind the holder in FIFO order. Each critical section takes . Ignoring the per-arrival half-offset, the thread at queue position (for , since one holds the lock) waits for the sections ahead of it. What is the average wait across the queued threads?
Recall Solution
See Figure s01 below: the four queued threads wait (the bars), and the dashed line marks their mean. Sum . Average over 4 threads: So the exact answer is . The parent's shorthand is a slightly lower approximation — good to recognize the difference, but is correct for this discrete FIFO queue.
Figure s01 — FIFO spin-lock queue. Each queued thread's bar length is its wait (); the dashed line is the mean .

L3.2 — Energy wasted by spinning
Each spin iteration costs nJ and takes . Lock held for , and threads spin the whole time. Using compute the wasted energy.
Recall Solution
Spins per thread iterations. This growth with contention () is exactly why blocking locks exist: a sleeping thread burns ~0 energy. See Thread scheduling.
Level 4 — Synthesis
L4.1 — Why the generation counter?
A reusable barrier for threads uses count, threshold=3, and (crucially) a generation counter, plus a condition variable (CV) — the "waiting room" defined in the vocabulary list above. Suppose the last thread wakes the others and resets count = 0 but does NOT advance generation. Thread A (fast) is woken, loops back, and immediately calls barrier_wait for phase 2 before thread B has been scheduled. Explain, step by step, what goes wrong — and how advancing generation prevents it.
Recall Solution
Without generation:
- Phase 1: A, B, C all arrive,
counthits 3, C resetscount=0and broadcasts on the CV. - A wakes, exits, races into phase 2, does
count++→count = 1. - B (from phase 1) was slow to wake; it re-checks the condition. But the condition ("has the barrier opened?") looks the same as a fresh phase-2 arrival — B and A's states are tangled. B may either slip through without all of phase-2 arriving, or get permanently stuck waiting for a
count == 3that already got reset. With generation:
- Each thread records
my_gen = b->generationbefore waiting on the CV. - The last thread does
generation++, so old and new episodes have different numbers. - A woken thread proceeds only when
b->generation != my_gen— i.e. when its episode truly ended, not merely when the CV was signalled. This makes each barrier crossing logically distinct, killing the reuse race. Related idea: spurious wakeups in Memory consistency models require the same "loop and re-check a real condition" discipline.
L4.2 — Ordering guarantee
Two threads share flags initialized to 0:
Thread 1: x = 1; r1 = y;
Thread 2: y = 1; r2 = x;
On a machine with a weak memory model, can we observe r1 == 0 && r2 == 0? What primitive/instruction forbids it?
Recall Solution
Yes, on a weak model both can read 0: each core may reorder its own store after its load, so both loads see the old 0 before either store is visible. This is the classic "store buffering" outcome.
The fix is a memory fence (a full barrier instruction) placed between the store and the load in each thread. It forces the store to become globally visible before the load executes, making r1 == 0 && r2 == 0 impossible. This is exactly the territory of Memory consistency models and is enforced under the hood by Cache coherence protocols.
Level 5 — Mastery
L5.1 — Build a counting semaphore from a lock + condition variable
A semaphore with value allows up to threads into a region at once (a lock is the special case ). wait() decrements (blocking if would go negative); signal() increments (waking a waiter). Write correct pseudocode using a lock and a cond variable (CV), and argue mutual-exclusion-style correctness.
Recall Solution
typedef struct { int S; lock_t lock; cond_t cv; } sem_t;
void sem_init(sem_t *s, int val) {
s->S = val; lock_init(&s->lock); cond_init(&s->cv);
}
void sem_wait(sem_t *s) {
lock(&s->lock);
while (s->S <= 0) // LOOP, not if: guards spurious wakeups
cond_wait(&s->cv, &s->lock);
s->S--; // now safe to take a slot
unlock(&s->lock);
}
void sem_signal(sem_t *s) {
lock(&s->lock);
s->S++;
cond_signal(&s->cv); // wake one waiter
unlock(&s->lock);
}Correctness:
- The
lockgives mutual exclusion onS, so no lost updates on the counter. - The
while(notif) re-checksS > 0after waking — essential against spurious wakeups AND against another thread grabbing the freed slot first. - Setting
val = 1gives a plain mutex; largervaladmits multiple threads (a resource pool of size ).
L5.2 — Deadlock by lock ordering
Thread 1 does acquire(A); acquire(B);. Thread 2 does acquire(B); acquire(A);. Show the interleaving that deadlocks, then give the ONE rule that prevents ALL such deadlocks.
Recall Solution
Deadlock interleaving:
- T1 acquires A. 2. T2 acquires B. 3. T1 waits for B (held by T2). 4. T2 waits for A (held by T1). Circular wait → nobody proceeds. The universal fix: impose a global lock ordering — every thread must acquire locks in the same fixed order (say, always A before B). Then a cycle is impossible: no thread ever holds a "higher" lock while waiting for a "lower" one, so one of Coffman's four conditions (circular wait) is broken. See Deadlock and livelock.
Recall Quick self-test cloze
A lock built on a plain bool fails because check-and-set is not ::: atomic (two separate steps let another thread interleave) The expected per-arrival spin wait for a hold of is ::: A barrier needs a generation counter to survive ::: reuse across back-to-back phases (the reuse race) The four-condition deadlock is broken by a global ::: lock ordering (prevents circular wait)