skip to content

questions

5

What is a spinlock, and how does it differ from a blocking (parking) lock in terms of what a waiting thread actually does and what it costs?

level: middleimportance: must knowfreq 56%

answer

  1. spin = keep the CPU, buy latency; block = free the CPU, pay latency
  2. cache-line transfer ~ tens of ns vs context switch ~ µs
  3. spinner burns a core doing nothing
  4. context switch also costs cold caches and TLB
  5. never spin if the critical section can block

basics

~20 s

A spinlock makes a waiting thread loop on an atomic test until the lock frees, keeping its CPU and never involving the scheduler. A blocking lock parks the thread so the CPU runs something else, paying a context switch on both sleep and wake.

solid answer

~60 s

Both are mutual exclusion; they differ in what waiting *is*. A **spinlock** busy-waits: the thread loops on an atomic read-modify-write ("is it free? try to take it") until it succeeds. It keeps its CPU the whole time and never talks to the scheduler, so wake-up latency after the holder releases is on the order of tens of nanoseconds — roughly a cache-line transfer. The cost is that every spinning cycle is a cycle no other thread can use. A **blocking lock** parks: the waiter registers itself on a wait queue and is descheduled, freeing the CPU. Sleeping and waking cost a context switch each, typically a few microseconds all-in once you count scheduler work and the cold caches and TLB entries the thread returns to. So it is a straight trade: spinning burns CPU to buy latency; blocking gives up latency to reclaim CPU. Spinning wins for very short holds on an under-subscribed machine; blocking wins for long or unpredictable holds, and is the only safe choice if the critical section can itself block.

code

text · 14 lines
text
# spinlock: waiter stays on-CPU
acquire():
    while atomic_test_and_set(lockword) == LOCKED:
        cpu_pause_hint()          # SMT/power hint, still spinning
release():
    atomic_store(lockword, FREE)

# blocking lock: waiter leaves the CPU
acquire():
    if not try_lock():
        enqueue(self); park()     # descheduled until woken
release():
    unlock()
    if waiters: wake_one()        # explicit wake required

go deeper

for a junior

State the mechanical difference — busy loop versus descheduled thread — and that spinning wastes CPU while blocking costs a context switch.

for a middle

Quantify it: cache-line transfer in tens of nanoseconds against a context switch in microseconds, plus the cold-cache aftermath, and give the short-hold versus long-hold rule.

for a senior

Bring in memory-system effects (ping-pong, SMT siblings, power) and the hard precondition that the holder must be running and must not block.

for a principal

Frame it as a policy choice about where CPU time is best spent under a given oversubscription level, and note that every production lock is a hybrid whose only real question is where the crossover sits.

## Two ways to wait Every mutual-exclusion primitive must answer one question: what does a thread do when the lock is already held? There are exactly two families of answer. **Spin (busy-wait).** Loop, repeatedly attempting an atomic operation on the lock word until it succeeds: ``` acquire(): while atomic_swap(lock, LOCKED) == LOCKED: /* spin */ release(): atomic_store(lock, FREE) ``` The thread stays runnable and on-CPU. Nothing is told to the operating system. When the holder writes FREE, the spinner's next iteration observes it, and the hand-off costs roughly a cache-line transfer between cores — tens of nanoseconds. **Block (park).** Record the waiter on a queue associated with the lock and ask the scheduler to deschedule it. The CPU is immediately available to other work. When the holder releases, it must explicitly wake a waiter, and that waiter must be rescheduled onto a CPU before it can proceed. ## The cost model Orders of magnitude worth memorising (they vary by hardware but the ratios are stable): - Uncontended atomic operation on a cache line you already own: a few nanoseconds. - Cache-line transfer between cores: tens of nanoseconds; across sockets, over a hundred. - Voluntary context switch (park) plus later wake and reschedule: single-digit microseconds of direct cost. - Indirect cost of a context switch: the thread resumes with cold L1/L2 caches, branch predictors and TLB entries. The *real* cost is frequently larger than the direct cost and is invisible in a microbenchmark of the lock itself. That is roughly a hundred-fold gap between the two waiting strategies. It is why the rule of thumb is expressed as a comparison: **spin if the expected hold time is well below the round-trip cost of parking and waking; otherwise block.** ## What spinning costs that is easy to forget - **The CPU itself.** A spinning thread is 100% busy doing no work. On a machine with more runnable threads than cores, that CPU time is stolen from a thread that could have made progress — possibly the lock holder itself. - **Memory-system traffic.** A naïve spin loop that hammers an atomic swap acquires the cache line exclusively on every iteration, ripping it away from the holder and from other spinners. With several waiters this becomes cache-line ping-pong, and the holder itself slows down — you spin harder and the lock is released later. - **Power and thermal budget.** On laptops and dense servers, busy loops burn watts and can pull down turbo frequencies for the cores doing real work. - **Sibling hyperthreads.** A tight loop competes with the SMT sibling sharing the same execution units, so spinning can halve the throughput of the sibling that is doing real work. This is why architectures provide a pause/yield hint instruction for spin loops. ## What blocking costs that is easy to forget - **Latency floor.** No matter how briefly the lock is held, a parked waiter cannot resume faster than a wake plus a schedule. - **The wake-up must be reliable.** The holder has to notice that a waiter exists and issue the wake; a missed wake-up is a hang. Blocking implementations therefore carry more state and more subtle races than a spinlock's single word. - **Convoying.** Once threads start parking on a hot lock, they queue and get handed the lock in a strict sequence, each paying a wake latency — throughput can fall off a cliff at the point contention starts. ## Where each is actually used Spinlocks dominate inside operating-system kernels and low-level runtimes for very short critical sections — updating a scheduler run queue, flipping a few fields — where the code cannot block anyway (an interrupt handler has no thread to park) and where the holder is guaranteed to be running and to finish in a bounded, tiny number of instructions. Blocking locks dominate in application code, where a critical section may perform I/O, allocate memory, take a page fault, or simply be long enough that burning a core is indefensible. ## A hard constraint, not a preference If the critical section can block or be preempted for an unbounded time, spinning is not merely slower — it is wrong. A thread spinning for a holder that is itself blocked on I/O wastes a core for milliseconds. On a single CPU with a non-preemptive scheduler it is worse than wasteful: the spinner cannot yield, so the holder can never run, and the spin is an infinite loop. This is why the correctness precondition for a spinlock is "the holder is running on another core and will finish quickly". ## The obvious synthesis Because each strategy wins in a different regime, real implementations rarely commit to one: they spin briefly and then park, which is adaptive spinning. But you should be able to state the pure trade first — spinning buys latency with CPU, blocking buys CPU with latency — because every hybrid is just a policy for choosing the crossover point.

  • Why is spinning on a single-CPU system with a non-preemptive scheduler not just inefficient but incorrect?
    The spinner occupies the only CPU and never yields, so the lock holder cannot be scheduled to finish its critical section and release. The wait becomes unbounded — a livelock rather than a delay. Spinning is only sound when the holder can genuinely run in parallel on another core, which is why the precondition is "holder is currently running".
  • Why is the cost of parking usually larger than the raw context-switch number suggests?
    The direct scheduler cost is only part of it. When the thread resumes it has lost its L1/L2 cache contents, TLB entries and branch-predictor state, so the code after the lock runs slower than it did before it parked. It may also resume on a different core, losing NUMA locality. Microbenchmarks that measure only lock/unlock latency systematically underestimate this.
  • If spinlocks are so cheap for short sections, why don't application-level mutexes just spin?
    Application critical sections are unpredictable — they can allocate, fault, do I/O, or be preempted — so the expected hold time is neither short nor bounded, and a spin would waste a full core for milliseconds. Application threads also usually run on an oversubscribed machine, where a spinner may be stealing the very CPU the holder needs. Most real mutexes therefore spin only briefly and then park.

Waiting for a shared printer. Spinning is standing at the printer refreshing the display every second: you get the job the instant it frees, but you do no work meanwhile. Blocking is going back to your desk and being paged: you get useful work done, but you lose half a minute walking back.

saying these in an interview costs you the question

  • Claiming spinlocks are always faster because they avoid system calls
  • Ignoring that a spinning thread consumes a whole CPU
  • Believing a blocking lock is only about the context-switch instruction count and not the cache damage
  • Proposing a spinlock around a critical section that performs I/O
  • Assuming spinning is safe regardless of how many threads are running per core

context

open as a page

Under what conditions does busy-wait spinning outperform parking a waiting thread? Give the break-even reasoning, not just a rule of thumb.

level: middleimportance: should knowfreq 48%

basics

~20 s

Spin when the expected remaining hold time is shorter than the round trip of parking and waking, and when a spare core exists so the holder can still run. Long, unpredictable or blocking critical sections, and oversubscribed CPUs, favour parking.

open as a page

Why do real spin loops read the lock word before attempting an atomic write, and why do they add backoff and adaptive spin limits instead of retrying as fast as possible?

level: seniorimportance: should knowfreq 40%

basics

~20 s

An atomic write takes the cache line exclusively, so hammering it makes every spinner steal the line from the holder and each other. Reading first (test-and-test-and-set) spins on a shared copy; backoff and adaptive limits cut the remaining traffic and stop pointless spinning.

open as a page

Explain the futex-style design used by modern mutexes: why an uncontended acquire needs no call into the operating system, and what has to happen once the lock is contended.

level: seniorimportance: should knowfreq 38%

basics

~20 s

The lock state is an ordinary word in user memory, so an uncontended acquire is one atomic compare-and-swap — a few nanoseconds, no kernel. Only when a thread must actually wait does it make a wait system call naming that address; the kernel queues it and the releaser wakes it.

open as a page

A service that uses busy-wait spinning performs well on dedicated hardware but collapses when moved onto shared or virtualised machines. Explain the mechanism behind that collapse and what mitigations exist.

level: principalimportance: should knowfreq 34%

basics

~20 s

Spinning assumes the lock holder is running in parallel. Under oversubscription the holder gets preempted, so spinners burn whole scheduling quanta waiting for a thread that cannot run — wasting the very CPU it needs. Fix by bounding spins and parking, or by not oversubscribing.

open as a page