Why this matters: this is the rare interview where you can write every line on the page and still have most of your time left for defense. Use that. The walkthrough below builds the lock in three deliberate passes — correct, then coherent, then calm — because presenting the evolution is the demonstration of understanding.
Pass 1 — TAS: correct
struct spinlock { atomic_int locked; }; // 0 = free, 1 = held
void lock(struct spinlock *l) {
while (atomic_exchange_acquire(&l->locked, 1) == 1)
; // lost — retry
}
void unlock(struct spinlock *l) {
atomic_store_release(&l->locked, 0);
}Five lines, and two of them carry the whole correctness story — narrate both:
The exchange returns the old value. Getting 0 back means I flipped free-to-held; the hardware guarantees exactly one such winner. That's mutual exclusion in one primitive.
The annotations are the lock. acquire on the winning exchange, release on the store of 0 — the handshake from lesson 02 that makes the previous holder's writes visible to the next. Say the failure plainly: with relaxed ordering this still "takes turns," but the turns can't see each other's work — mutual exclusion around data races. An interviewer who hears you volunteer that has stopped worrying about your ordering model.
This lock is correct and complete. It's also, under contention, a coherence storm — which you say out loud as the reason for pass 2.
Pass 2 — TTAS: coherent
void lock(struct spinlock *l) {
for (;;) {
while (atomic_load_relaxed(&l->locked) == 1)
; // spin on a READ (shared line)
if (atomic_exchange_acquire(&l->locked, 1) == 0)
return; // won
} // lost the race — spin again
}The inner while is the entire change: waiters now watch a plain load of their own cached copy — shared state, zero traffic — and reach for the expensive RMW only when a release makes winning plausible. Narrate the two ordering choices: the spinning load can be relaxed, because it decides nothing by itself — it's a hint that a real attempt is worthwhile. The acquire is on the exchange, the operation that actually wins the lock; that's the moment the previous section's writes must become visible, and putting the fence where the decision happens (not on every hint-read) is precision the round rewards.
Also worth a sentence: after losing the exchange, control falls back to the read-spin — never hammer the RMW in a tight loop, that's pass 1's mistake sneaking back in.
Pass 3 — backoff: calm
void lock(struct spinlock *l) {
int pause = MIN_PAUSE;
for (;;) {
while (atomic_load_relaxed(&l->locked) == 1)
cpu_relax(); // polite spin (lesson 04)
if (atomic_exchange_acquire(&l->locked, 1) == 0)
return;
backoff(pause); // lost at the handoff:
pause = min(pause * 2, MAX_PAUSE); // back off exponentially
}
}The backoff sits after a lost exchange — the signal that the handoff was contended — not inside the read-spin, which is already quiet. Doubling with a cap keeps the trade honest: less stampede per release, bounded worst-case napping. cpu_relax() is a placeholder this chapter cashes in lesson 04.
try_lock, since it's free
bool try_lock(struct spinlock *l) {
return atomic_load_relaxed(&l->locked) == 0
&& atomic_exchange_acquire(&l->locked, 1) == 0;
}One quiet read to skip hopeless attempts, one exchange to win — TTAS's single iteration with a verdict. Offering it unprompted costs four lines and signals API instincts.
What you'd say about complexity
Uncontended: one exchange to lock, one store to unlock — a handful of nanoseconds, the whole reason spinlocks exist. Contended: the honest metric isn't instruction count but coherence traffic per handoff — TAS generates it continuously while anyone waits; TTAS+backoff pays roughly one line-bounce per release. Memory: one word (padded to a cache line if the lock shares space with hot data — the ring buffer's false-sharing lesson applies to locks verbatim).
Key takeaway
Build it in three narrated passes: TAS proves mutual exclusion with acquire-on-exchange and release-on-store carrying the section's writes; TTAS moves the wait onto a relaxed read of a shared cache line and saves the acquire for the exchange that actually decides; backoff after a lost handoff blunts the stampede with a capped exponential pause. Every annotation has a one-sentence justification — deliver them as you type, and the dozen lines defend themselves.