skip to content

You are designing a heavily used shared data structure and a colleague proposes replacing its mutex with a hand-written non-blocking algorithm on the grounds that "locks are slow". How do you decide whether a non-blocking design is the right call, and what would you propose instead if it is not?

level: principalimportance: should knowfreq 30%

answer

  1. Non-blocking buys scheduler independence and tail latency, not throughput
  2. Contention serialises in hardware either way
  3. Costs: ordering, reclamation, untestability, maintenance
  4. First fix: partition, batch, snapshot, single owner, shorter critical section
  5. If truly needed, take a proven implementation; never hand-roll

basics

~20 s

Non-blocking algorithms buy tolerance of preemption and failure and bounded tail latency, not throughput — contention still serialises in hardware. Choose them for delay-intolerant or non-blockable contexts, and prefer proven library implementations. Otherwise reduce sharing: partition per core, batch, use immutable snapshots, or keep a short-held lock.

solid answer

~60 s

Start by rejecting the premise: uncontended locks are cheap, and under contention a non-blocking retry loop funnels through the same contended cache line, so it rarely wins on throughput. The real currency of non-blocking design is **independence from the scheduler** — no thread's preemption, page fault, priority inversion, runtime pause or death can stall the others — plus bounded tail latency and usability where blocking is forbidden. So I ask: what is the actual requirement? If it is throughput, the winning move is usually to remove the sharing — partition state per core or per shard, batch updates, publish immutable snapshots, or give the structure a single owning thread fed by a queue. If it is tail latency or preemption tolerance, a non-blocking structure is justified — and then I take a reviewed library implementation rather than hand-rolling, because correctness here is subtle: memory ordering, safe reclamation of removed nodes, and testing that can actually find rare interleavings. And I would insist on a measurement at realistic contention before either change.

go deeper

for a junior

Know that removing a lock does not automatically make code faster and that non-blocking algorithms are much harder to get right.

for a middle

Explain that contention still serialises at the hardware level and name the real benefits of non-blocking designs: no dependence on a lock holder being scheduled.

for a senior

Run the decision procedure — measure, name the requirement, try to remove sharing, and only then consider a proven non-blocking implementation — and describe the testing burden honestly.

for a principal

Frame it as a total-cost decision across correctness risk, testing capability and maintenance lifetime, set the standard that such algorithms are adopted rather than authored, and specify the benchmark and latency objective that will govern the choice.

## Reframe the claim "Locks are slow" is a folk belief inherited from an era of naive lock implementations. Today, an uncontended lock acquisition is a handful of instructions on an already-owned cache line, and mature implementations spin briefly before parking a waiting thread, so short critical sections rarely reach the kernel. Meanwhile, a lock-free retry loop under contention repeatedly acquires exclusive ownership of the same cache line and throws work away on failure. Both designs serialise at the same physical bottleneck. The honest comparison is therefore not "lock versus lock-free" but "how much contended sharing does the design have at all". ## What non-blocking actually buys 1. **Scheduler independence.** A blocking design's worst case is the lock holder being descheduled at the wrong instant — preempted by a higher-priority thread, taking a page fault, hitting a runtime pause, or being killed. Every waiter then waits for the scheduler, not for the work. Lock-free algorithms have no holder, so no such dependency exists. 2. **Bounded tail latency.** Where the requirement is a percentile, not a mean, the blocking worst case may be unacceptable even though the average is fine. 3. **Contexts that must not block.** Signal handlers, interrupt paths, real-time threads, and code that must remain usable when other participants may die. 4. **Priority inversion avoidance** without needing inheritance protocols. Notice that none of these is "more operations per second". If someone's justification is throughput, they have probably misdiagnosed the problem. ## The costs - **Correctness is genuinely hard.** Memory ordering must be right; intermediate states must be legal and detectable; multi-step updates need helping so a stalled thread cannot strand the structure. Reclaiming nodes that other threads may still be dereferencing is a research-grade subproblem in its own right, and in environments without automatic memory management it is where most hand-rolled attempts fail. - **Testing is weak.** Bugs appear as rare interleavings under specific hardware and load. Ordinary unit tests do not find them. You need history-based linearizability checking, systematic interleaving exploration, long stress runs across different machines — and you must budget for that. - **Maintenance burden.** The next engineer to touch the file must reconstruct an invariant argument that is not visible in the code. A subtle change silently breaks it. - **Tail behaviour can be worse.** A retry loop under heavy contention can starve an individual thread indefinitely; lock-freedom guarantees system progress, not per-thread bounds. ## The decision procedure I would use 1. **Measure.** Is the structure actually the bottleneck, at realistic contention, with realistic operation mix? Very often the hot path is elsewhere, or contention is far lower than assumed. 2. **Name the requirement.** Throughput, mean latency, tail latency, or fault/preemption tolerance? Only the last two point at non-blocking designs. 3. **Try to remove the sharing.** In rough order of payoff: - **Partition**: per-core or per-shard state, combined on read. Contention disappears rather than being converted into retries. - **Batch**: amortise each shared update over many logical operations. - **Read-mostly**: build a new immutable version and publish it, so readers never write shared state. - **Single ownership**: one thread owns the structure and others send it work; hand-offs batch, ownership transfers do not. - **Shorter critical sections**: move allocation, formatting, I/O and computation outside the lock. Frequently this alone dissolves the problem. 4. **If non-blocking is warranted, do not hand-roll it.** Use a well-reviewed implementation from a standard library or an established source. Published algorithms have known proofs and known pitfalls; a bespoke one has neither. 5. **Consider a narrower non-blocking structure.** Many systems need only a single-producer/single-consumer ring buffer or a bounded queue, which are dramatically simpler and faster than a general lock-free container. 6. **Re-measure and keep the benchmark.** Whatever is chosen, the benchmark that justified it should live in the repository so a later change cannot quietly undo the result. ## What I would say to the colleague That I agree the structure deserves attention, that lock-freedom is a guarantee about scheduling rather than a speed feature, and that we should first establish which requirement is unmet. If it turns out to be a latency percentile driven by preemption of a lock holder, non-blocking is the right tool and we should adopt an existing implementation. If it is throughput, the higher-leverage change is almost always to stop sharing the hot state — and that change is also simpler to review, test and maintain, which matters more over the life of the system than any single benchmark result.

  • Which concrete situations do justify a non-blocking design?
    Code that cannot block by construction — signal or interrupt context, real-time threads with hard deadlines — and systems where a participant may be preempted, paused or killed while holding shared state, such that others must still proceed. Also cases where a latency percentile rather than a mean is the requirement and measurement shows the tail is dominated by waiting on a descheduled holder. In each of these the guarantee, not the speed, is what is being bought.
  • Why is testing a hand-written non-blocking structure so much harder than testing a lock-based one?
    A lock-based structure reduces the state space to sequential executions of the critical section, so ordinary tests exercise nearly all of it. A non-blocking structure is correct only across every legal interleaving and memory-ordering outcome, and the failures are rare, hardware-dependent and often non-reproducible. Adequate testing needs history-based linearizability checking, systematic interleaving exploration and long stress runs on multiple machine types, which is a substantial ongoing cost most teams do not budget for.
  • How would you reduce contention without changing the synchronisation mechanism at all?
    Shrink what is done while holding the lock: move allocation, serialisation, logging, computation and any I/O outside the critical section so it protects only the state mutation. Then attack the sharing itself by partitioning state per shard or per core and combining on read, and by batching updates so many logical operations cost one shared update. These changes are cheap to review and often deliver more than swapping in a non-blocking algorithm.

saying these in an interview costs you the question

  • Asserting that lock-free is faster than locking as a general rule
  • Ignoring safe reclamation of removed nodes in a hand-written design
  • Choosing non-blocking for throughput when the requirement is really tail latency, or vice versa
  • Hand-rolling a general-purpose lock-free container instead of using a reviewed implementation
  • Deciding without measuring at realistic contention, or without keeping the benchmark afterwards

context