skip to content

A mutual-exclusion lock can hand ownership to the longest-waiting thread in strict FIFO order, or let whichever thread happens to be running acquire it first. Compare the two policies and explain why the fair one usually delivers lower throughput.

level: seniorimportance: should knowfreq 40%

answer

  1. Fair = bounded waiting; unfair = only deadlock-freedom
  2. Handoff cost (µs) vs critical section (ns) → lock idles
  3. Barging thread has hot caches; handoff is cold + cross-core
  4. Convoying: FIFO keeps waiters bunched in lock-step
  5. Middle ground: barge at most k times, then hand off

basics

~20 s

Fair/FIFO locks guarantee bounded waiting, so no thread starves, but every handoff waits for the next waiter to be rescheduled and runs with cold caches. Unfair/barging locks let a running thread take the lock immediately — far higher throughput, at the cost of an unbounded tail and possible starvation.

solid answer

~1 min

**Fair (FIFO/queued)**: on release, ownership is handed to the head of the wait queue. Guarantees bounded waiting and a predictable tail, and makes lock-wait time roughly proportional to queue position. **Unfair (barging)**: on release the lock is simply freed; any thread — a fresh arrival or a spinner already on a core — may take it. Waiters are also woken, but they must be rescheduled first and frequently lose the race. The throughput gap comes from three effects: 1. **Handoff latency / lock idle time.** Waking a parked thread costs microseconds; the critical section may take nanoseconds. Under fairness the lock sits idle for that whole wake-up window on every handoff, so throughput is capped by context-switch cost rather than by the critical section. 2. **Cache locality.** A barging thread's data and the lock line are already hot on its core; the handed-off thread starts cold and often on another core, adding cache-line transfers. 3. **Convoying.** Fair locks force lock-step ordering, so one slow holder makes every waiter inherit its delay, and threads stay bunched instead of spreading out. Pick unfair by default with short critical sections; pick fair when a bounded tail or per-tenant guarantee matters more than raw throughput, or add bounded overtaking (barge at most k times) as a middle ground.

code

text · 5 lines
text
critical section        ~100 ns
unpark + reschedule   ~2000-5000 ns

fair handoff : lock unusable for the wake-up window  -> ~1 acq / 3 us
barging      : next acquirer already on CPU          -> ~1 acq / 150 ns

go deeper

for a junior

State the contrast: fair means first-come-first-served and nobody starves; unfair means whoever is already running grabs it, which is faster but can leave a waiter behind.

for a middle

Explain the mechanism — wake-up latency leaving the lock idle, cold caches on handoff — and name bounded waiting as the property fairness provides.

for a senior

Quantify it (microsecond unpark versus nanosecond critical section), discuss convoying and preemption, and pick a policy from an explicit objective: mean throughput versus p999.

for a principal

Argue at the system level: fairness as an isolation mechanism between tenants or workload classes, bounded-overtaking hybrids, and the stronger move of removing the shared lock entirely via sharding or single-owner queues.

## The two policies Every blocking mutex has to answer one question: when the owner releases, who gets it next? - **Fair / FIFO**: the lock maintains a queue; release transfers ownership directly to the head of that queue (a *handoff*), or at least the queue's order is enforced so no one else may take it. Property: **bounded waiting** — once you enqueue, at most the threads already ahead of you enter first. - **Unfair / barging**: release simply marks the lock free. A waiter is signalled, but any runnable thread that calls acquire in the meantime can win. Property: only **deadlock-freedom** — someone progresses, but no promise for you. Both are correct in the safety sense; they differ purely on liveness guarantees and performance. ## Why fairness costs throughput **Handoff latency dominates short critical sections.** Parking and unparking a thread costs on the order of a few microseconds (syscall, scheduler run-queue insertion, possible IPI to another core, context switch). A well-written critical section may run in tens or hundreds of nanoseconds. Under strict handoff the lock is unavailable during the entire wake-up window, so the *effective* critical-section length becomes the wake-up cost. Throughput is then bounded by roughly 1 / (wake-up latency) acquisitions per second, potentially one to two orders of magnitude below what a barging lock achieves, where the next acquirer is already running. **Cache locality.** The barging thread is on a CPU right now; the lock word, and often the protected data, are hot in its L1/L2. The handed-off thread starts on a different core, so the lock cache line and every protected line must migrate — each migration is tens to hundreds of cycles. Fairness deliberately maximises the number of migrations by rotating ownership among all waiters. **Convoying.** With strict FIFO the whole queue proceeds in lock-step. If one holder is descheduled or takes a page fault while holding, every waiter behind it stalls and the group stays bunched: they finish together, contend together, queue together again. Unfair locks let threads drift out of phase, which reduces contention naturally. **Preemption sensitivity.** In a fair lock the next owner may be preempted before it even runs; the lock stays blocked until the scheduler comes back to it. Barging lets any other runnable thread use the lock in that gap, so the resource is not held hostage by one scheduling decision. ## What fairness buys - **Bounded tail latency.** The p999 of lock-wait becomes a function of queue length rather than of luck. For latency-SLO systems this can matter more than mean throughput. - **No starvation.** Every waiter is guaranteed to be served, which matters when the contending population is heterogeneous — for example, a background scanner competing with user requests, or many tenants on one resource. - **Predictability and easier reasoning.** Wait time correlates with observable queue depth, so capacity planning and back-pressure decisions become tractable. A useful framing: unfair locks optimise the **mean**, fair locks optimise the **maximum**. If your objective function is requests per second, choose unfair. If it is "99.9% of requests under X ms", fairness (or bounded overtaking) is often the cheaper way to get there than shaving the mean. ## The middle ground Real lock implementations rarely sit at an extreme: - **Bounded overtaking / eventual fairness**: allow barging, but if a waiter has been overtaken k times, or has waited longer than a threshold, switch that lock into handoff mode until the waiter is served. This keeps nearly all the throughput while capping the tail. Several production mutexes use exactly this design. - **Spin-then-park**: spin briefly before parking, so short critical sections never pay wake-up cost, and only long waits go to the queue. - **Queue-based spin locks (MCS/CLH-style)**: each waiter spins on its own cache line and is granted the lock in order — fair *and* scalable, at the cost of requiring spinning, which is only viable when holders are never preempted and hold times are tiny. - **Cohort or NUMA-aware locks**: prefer handing the lock to a thread on the same socket to preserve locality, with a bound on how long one socket may keep it — trading a little fairness for a lot of locality. ## Choosing in practice Ask four questions. How long is the critical section relative to a context switch? If shorter, fairness is expensive. How contended is the lock? Uncontended locks make the policy irrelevant. Is the workload homogeneous? If all threads do the same work, unfairness is statistically self-correcting; if some class can be systematically slower to wake, it will starve. What is the objective — throughput or bounded tail? State the objective first, and the policy follows. And remember the meta-answer an interviewer wants: the best fix is usually to reduce contention, not to tune the policy. Shorter critical sections, sharding the data, per-thread or per-core state, read-mostly structures, or replacing the lock with a queue that a single owner drains all remove the question entirely.

  • When would you deliberately accept the throughput loss and enable a fair lock?
    When a bounded tail or per-class guarantee is the actual requirement: multi-tenant resources where one tenant must not monopolise, latency SLOs expressed at p999, or a background job that would otherwise never acquire the lock. It also helps when critical sections are long relative to a context switch, because then the handoff cost is amortised and fairness is nearly free.
  • What is bounded overtaking and why is it usually a better default than either extreme?
    The lock allows barging normally, but tracks how long the head waiter has been queued or how many times it was bypassed; past a threshold it switches to strict handoff until that waiter is served. You keep the hot-cache, no-idle-lock throughput of barging for the common case while capping worst-case wait, so mean and maximum are both acceptable.
  • Does making a lock fair remove convoying?
    No — it can make convoying worse. Strict FIFO forces all waiters to proceed in lock-step behind a slow or descheduled holder, so the group stays synchronised and re-contends together. Convoying is attacked by shortening or removing the critical section, avoiding preemption while holding, or replacing the shared lock with per-thread or sharded state.

A supermarket with one till. Strict queueing is fair but the till idles while each next customer walks up; letting whoever is already standing there pay keeps the till busy — and the person at the back may never reach it.

saying these in an interview costs you the question

  • Assuming a fair lock is strictly better because "fairness is correct" — it trades throughput for a bounded tail and must be chosen deliberately.
  • Not knowing that the wake-up cost, not the critical section, sets the ceiling for strict-handoff throughput.
  • Claiming barging causes incorrect results; both policies are equally safe, they differ only in liveness and performance.
  • Believing fairness prevents convoying, when lock-step ordering is exactly what sustains a convoy.
  • Tuning the fairness policy instead of reducing contention (sharding, shorter sections, read-mostly structures).

context