6.1.8 · D5Parallelism & Multicore
Question bank — Synchronization primitives (locks, barriers)
Look at the picture of the three concepts this bank tests before you start — it names where each trap lives.

The atomic read-modify-write, visually

True or false — justify
The simple flag lock (check lock == true, then set lock = true) guarantees mutual exclusion.
False — two threads can both read
false in the gap between the check and the set (the red gap in the figure above), both pass the loop, and both write true. The check-and-set is not one indivisible step.Test-and-Set (TAS) works because the compiler reorders the read and write together.
False — it works because the hardware makes the read-modify-write one atomic instruction, enforced by the cache coherence protocol, not because of any compiler trick.
An atomic Test-and-Set alone is enough to protect shared data across the critical section.
False — atomicity stops the acquire itself from racing, but without a memory fence the CPU may reorder the protected loads/stores outside the lock. You need acquire/release ordering too (see Memory consistency models).
A spinlock always wastes fewer CPU cycles than a blocking lock.
False — spinning is cheap only when the hold time is very short and cores are idle. On a single core or an oversubscribed system, spinning burns the very CPU the lock-holder needs to finish, making things worse.
Compare-and-Swap (CAS) can do everything Test-and-Set can, plus more.
True — CAS carries an
expected value, so it can conditionally update based on the current state, enabling lock-free structures; TAS always blindly writes 1, so it can only build a plain lock.A correct lock guarantees bounded waiting (no thread waits forever).
False — the TAS spinlock gives mutual exclusion and progress but not bounded waiting; an unlucky thread can lose every race and spin indefinitely. Fairness needs extra machinery like a ticket or queue.
Once the last thread in a barrier calls broadcast, resetting count = 0 is enough to make the barrier reusable.
False — a woken fast thread can loop into the next phase and increment
count before a slow thread from the previous phase runs, tangling the two episodes. You need a generation counter to keep phases distinct.A barrier and a lock solve the same problem.
False — a lock enforces mutual exclusion (one thread inside at a time); a barrier enforces rendezvous (all threads must arrive before any leaves). Different goals entirely.
If a lock is released but no thread is waiting, the release is wasted work.
False — releasing simply sets the flag to free (or unparks nobody); it is cheap and necessary so the next acquirer succeeds. There is no waste, just no one to wake.
park() / unpark() blocking locks eliminate all busy-waiting.
Mostly true, but there is a small window where a thread decides to sleep just as
unpark arrives (the lost-wakeup problem). The guard is to loop on a wake-up predicate — see the pattern below.The lost-wakeup guard and barrier generations, in code
Spot the error
acquire(&mutex);
if (error) return; // <-- what's wrong?
shared_counter++;
release(&mutex);Find the bug.
The early
return skips release, so the lock is held forever → deadlock. Every error path inside a critical section must release the lock first.void acquire(lock_t *l){ while(l->flag==1){} l->flag=1; }Why is this broken even though it "checks then sets"?
The check and the set are two separate operations; a second thread can slip into the gap. It must be a single atomic Test-and-Set / CAS, not two plain memory accesses.
if (b->count == b->threshold) { broadcast(&b->cv); }
else { wait(&b->cv, &b->lock); } // no loopWhat's the danger of wait without a surrounding loop?
A spurious wakeup (or a stale broadcast from a previous generation) can wake the thread while the condition is still false. Without a
while loop re-checking the generation, the thread proceeds too early.barrier_t b;
b.count = 0; b.threshold = 4; // then straight to barrier_waitWhat was forgotten?
The internal
lock and condition variable were never initialized (lock_init / cond_init), and generation wasn't set to 0. Using an uninitialized lock is undefined behaviour.// mutex with NO ownership tracking:
release(lock_t *l){ l->flag = 0; } // any thread may call thisThread B calls release on a lock held by thread A. What breaks, and how do real mutexes stop it?
Setting
flag = 0 frees the lock while A is still inside its critical section, so a third thread can enter — mutual exclusion is silently broken. Real mutexes store an owner id and reject a release (or double-release) from a non-owner, turning this into an error instead of corruption.Is E_waste = (T_c / t_spin) · (N−1) · E_instruction an error?
No — it is a correct estimate. Count the spins: a lock held for with each spin costing gives spins per waiter, times waiters, times per spin. It is here to show energy waste grows with contention , which is the real reason to prefer blocking locks.
Why questions
Why must Test-and-Set return the old value rather than just set the flag?
The old value is how a thread knows whether it was the one that acquired the lock (old was 0) or arrived too late (old was 1). Without the return value there's no way to distinguish winner from loser atomically.
Why does the barrier need to remember my_gen = b->generation before waiting?
So the waiter can tell "the barrier actually advanced to my next phase" apart from "someone signalled the CV." It only proceeds when
generation truly changes, not merely when woken.Why does cache coherence matter for atomic instructions?
The atomic read-modify-write must appear indivisible across cores; the coherence protocol gives one core exclusive ownership of the cache line for the duration, so no other core can observe or alter the value mid-operation.
Why do we need a fence and an atomic instruction — isn't atomicity enough?
Atomicity makes the acquire indivisible, but says nothing about ordering of the surrounding loads/stores. A fence stops the compiler/CPU from moving protected accesses outside the lock (see Memory consistency models).
Why is (per arrival) different from ?
is the expected wait behind one current holder for a randomly-timed arrival (on average, halfway through its hold). averages the wait across all FIFO queue positions, where later positions wait for every critical section ahead of them.
Why can spinning ever be better than blocking?
If the lock is held for less time than a context switch costs, spinning briefly avoids the expensive sleep/wake round-trip. This is exactly why some systems use adaptive/spin-then-block locks.
Why does forgetting to advance generation (only resetting count) still sometimes "work" in testing?
Under low contention threads rarely loop back fast enough to trigger the reuse race, so the bug hides. It surfaces only when a fast thread reaches the next barrier before a slow thread from the previous one — timing that stress tests expose but casual runs miss.
Edge cases
What does a barrier with threshold = 1 do?
The single thread arrives, immediately hits
count == threshold, advances the generation, and passes straight through — a no-op barrier, which is correct behaviour.What does a barrier with threshold = 0 do?
No thread can ever make
count equal 0 by incrementing (the first arrival sets count = 1 > 0), so with the == test the barrier never fires and any arrival hangs — a degenerate input that a robust barrier_init should reject (or treat as "fires immediately, letting everyone through").What happens if you release a lock that was never acquired?
The flag is forced to 0 (free), so a thread currently inside the critical section is no longer protected — another thread can now enter. This silently breaks mutual exclusion unless the mutex tracks ownership.
Two threads call acquire at the exact same instant on two cores. Who wins?
The coherence protocol serializes the two atomic operations onto the cache line, so exactly one TAS reads the old 0 and the other reads 1. Ties are impossible at the hardware level.
Zero threads ever call barrier_wait. What state is the barrier in?
It stays at
count = 0, generation = 0, waiting forever — harmless. The barrier only "fires" when the -th thread arrives; with no arrivals nothing blocks and nothing leaks.A thread calls barrier_wait for a phase, but one of the threads crashed and never arrives.
The barrier never reaches
threshold, so every arrived thread waits forever → a permanent stall. Barriers assume all participants are alive; missing participants cause a hang (a liveness failure, see Deadlock and livelock).Recall Self-check before moving on
One-line trap I still can't justify ::: Re-read that item's answer and trace it against the code in the parent note, then re-test yourself in an hour.