skip to content

Verifying Concurrent Code

Concurrency bugs are timing-dependent, so ordinary tests pass while production fails. This branch covers making async tests deterministic, hunting races with tools, and diagnosing live systems — skills interviewers probe to find engineers who have actually shipped concurrent code.

part ofComputer science fundamentalsoverview, primer and where to startread it →
on this pageshow

questions

20

A colleague tests asynchronous code by starting the work, sleeping for 100 milliseconds, and then asserting the result. Why is that approach problematic, and what should the test wait on instead?

level: juniorimportance: must knowfreq 70%

answer

  1. sleep = guess; fails both ways
  2. wait on a signal, not a duration
  3. quiescence = empty queue AND no active worker
  4. generous timeout, never a tuning knob
  5. healthy test never reaches the timeout

basics

~20 s

A fixed sleep is a guess about duration. Too short and the test fails on a loaded machine; too long and the suite crawls. Wait on a real completion signal instead — a future, a latch, a callback, or a quiescence check — with a generous timeout.

solid answer

~50 s

Sleeping hard-codes a guess, so the test fails in two directions at once. On a slow or contended CI runner the assertion runs before the work finishes and the test fails for no real reason; if you pad the sleep to be safe, every run pays that cost and a suite of hundreds loses minutes. The failure message is useless too: it says "not finished in 100 ms", not what went wrong. Wait on something the system actually signals: the future/promise the operation returns, a latch or semaphore released by the completion callback, an item published to a queue the test reads, or **quiescence** — drain until the work queue is empty and no worker is active. Give that wait a generous timeout (seconds), because a correct test returns immediately and only a broken one ever waits the full timeout. Best of all, remove waiting entirely by injecting a virtual clock or a scheduler the test itself drives.

code

text · 15 lines
text
BAD:
  submit(work)
  sleep(100ms)
  assert store.contains("x")

BETTER (completion handle):
  future = submit(work)
  future.await(timeout = 5s)      // returns as soon as work ends
  assert store.contains("x")

BETTER (no handle available):
  submit(work)
  scheduler.awaitQuiescent(timeout = 5s)
     // blocks until: queue.isEmpty() AND activeTasks == 0
  assert store.contains("x")

go deeper

for a junior

State the two-sided failure clearly (flaky when slow, wasteful when padded) and name at least one real thing to wait on: the returned future, a latch released by the callback, or a poll-until-condition helper.

for a middle

Add quiescence as the answer for fire-and-forget work, define it as empty queue plus no active task, and explain why the timeout should be generous rather than tuned.

for a senior

Frame it as removing wall-clock time from the test's correctness condition; mention virtual clocks and injected schedulers as the way to make the wait disappear entirely, and mention the diagnostic value of failing with state dumps.

for a principal

Talk about it as a suite-wide invariant — no raw sleeps in tests, enforced by lint — and about the cost curve: sleeps convert into CI minutes and, via flaky reruns, into lost trust in the suite.

## Why a fixed sleep is a bad wait `startWork(); sleep(100); assert(result)` bets that the work always finishes inside 100 ms on every machine that will ever run it. The bet fails in both directions. **Too short.** CI runners are shared, cold and CPU-throttled; a pause, a noisy neighbour or a container quota can stretch a 5 ms operation into 300 ms. The assertion then runs on an unfinished system and reports a failure that has nothing to do with the code. This is the classic recipe for a flaky test, and flaky tests get re-run, then muted, then deleted — so the sleep eventually destroys the coverage it was meant to provide. **Too long.** The obvious defence is to raise the sleep. Now every run pays the worst case even though the work is normally instantaneous. Three hundred such tests at 500 ms each is two and a half minutes of pure waiting per run, on every branch, forever. Sleep-based waits are the main reason async suites are slow. **Bad diagnostics.** A sleep-based failure says only "the expected state was not reached in time". It cannot distinguish "still running", "deadlocked", "threw and swallowed the exception", or "never started". ## What to wait on instead Wait on a *fact*, not on a *duration*: 1. **A completion handle.** If the operation returns a future/promise, block on it (with a timeout) or register a continuation the test observes. This is exact: the wait ends the instant the work ends. 2. **A synchronisation object the production callback touches.** A countdown latch, a semaphore, or a bounded queue the callback publishes to. The test blocks on it; the callback releases it. Zero polling, zero guessing. 3. **Quiescence (the drain check).** When the API gives you no handle — fire-and-forget work submitted to an executor — expose a way to ask the runtime "is everything settled?": pending queue empty **and** no task currently executing. Test helpers named `awaitIdle`, `drain`, or `runUntilQuiescent` do this. The composite condition matters: an empty queue alone is not idle, because a task may be mid-flight and about to enqueue more. 4. **Condition polling with a deadline.** As a fallback, poll the observable state every few milliseconds until it holds or a deadline passes. This is still nondeterministic in principle, but it converts "always wait X" into "wait only as long as needed, fail after a long ceiling". ## How to choose the timeout Set the timeout to the largest value that still keeps a genuine hang from stalling the build — typically 1–10 seconds. It is not a tuning knob for making the test pass: in a healthy run the wait returns in microseconds, so a big timeout costs nothing. If you find yourself increasing timeouts to fix failures, the test is timing-dependent and needs a real signal or a virtual clock, not a bigger number. ## When a sleep is still acceptable Rarely, and never as the sole synchronisation. Legitimate uses: asserting that something has *not* happened yet (a negative check has no event to wait for — but bound it and treat it as inherently weak), or deliberately exercising a timing path in a stress run. Even then, prefer advancing a virtual clock over sleeping on the real one. ## The deeper principle Deterministic async testing means every wait in a test corresponds to an event the system emits, not to wall-clock time. Once every wait is signal-based, test duration becomes proportional to actual work and failures point at causes instead of at timers.

  • If you must poll for a condition instead of waiting on a signal, what makes a poll loop safe?
    Poll a cheap, side-effect-free predicate on a short interval (a few milliseconds) against a long absolute deadline, and on expiry fail with a dump of the observed state rather than a bare timeout message. The deadline must be absolute, computed once, so retries cannot extend it indefinitely. Polling is still weaker than a signal because it can miss transient states, so use it only when the system exposes no completion event.
  • Is there any case where a sleep in a test is legitimate?
    Mainly for negative assertions — proving something has not happened yet, such as a debounced call not firing early — because there is no event to wait for. Even then it is a weak, slow check and a virtual clock is better: advance time by the debounce interval minus one tick and assert nothing fired. Sleeping to 'let things settle' before an assertion is never legitimate; that is a hidden race.

Waiting for a delivery by standing outside for exactly ten minutes versus waiting for the doorbell: the doorbell is exact and free, the ten minutes is both too long and sometimes too short.

saying these in an interview costs you the question

  • "Just increase the sleep until it passes" — treats the timeout as a tuning knob and hides a real race.
  • Thinking a long timeout slows the suite: a signal-based wait with a 10 s timeout costs nothing in a healthy run.
  • Claiming the empty work queue alone proves the system is idle, ignoring the task currently executing (which may enqueue more).
  • Adding a retry annotation instead of removing the timing dependency.
  • Treating sleep-based passes as proof of correctness — the test only proved 'finished within a guess'.

context

open as a page

What is a data race, and what does a dynamic race detector actually observe at runtime in order to decide that two memory accesses race?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A data race is two threads accessing the same memory location, at least one of them writing, with no synchronization ordering the accesses. A dynamic detector instruments every load, store and synchronization event at runtime and checks whether each pair of conflicting accesses is ordered.

open as a page

What is a virtual (fake) clock in a test suite, and which kinds of behaviour become testable when application code reads time through an injected clock instead of calling the system clock directly?

level: middleimportance: must knowfreq 58%

basics

~20 s

A virtual clock is a time source the test controls: it only advances when the test says so. Injecting it lets you test timeouts, retry backoff, cache expiry, rate limiting, and scheduled jobs instantly and deterministically, with no real waiting.

open as a page

A running service stops serving requests while its processor usage sits near zero. How do you use thread dumps — snapshots of every thread's stack and state — to work out what it is stuck on?

level: middleimportance: must knowfreq 62%

basics

~20 s

Take several dumps a few seconds apart and compare. Near-zero processor use means nobody is running, so look for threads blocked on a lock (and who owns it), waiting on a condition or a remote response, and for whole worker pools stuck in the same stack — that shared frame is the culprit.

open as a page

Simply running a multi-threaded test in a loop usually finds nothing. How would you design a stress harness that actually surfaces an interleaving bug?

level: middleimportance: must knowfreq 48%

basics

~20 s

Shrink the code under test so the bad window is a large fraction of it, start all threads from a barrier so they collide instead of running sequentially, perturb timing with randomized delays and varied thread counts, check a real invariant rather than a crash, and record the seed and observed state so a failure is reproducible.

open as a page

How can a runtime detect a true deadlock automatically, and how do you tell a deadlock apart from a livelock or from threads merely waiting on a slow remote call?

level: seniorimportance: must knowfreq 55%

basics

~20 s

A runtime builds a wait-for graph of which thread waits for which lock owner and reports a cycle as a deadlock. Distinguish by evidence: deadlock is permanent with identical stacks and no processor use; livelock burns processor with changing stacks and no progress; a slow remote call has threads in socket reads and eventually times out.

open as a page

In concurrency testing, what is a litmus test, and what question is it designed to answer?

level: juniorimportance: should knowfreq 26%

basics

~20 s

A litmus test is a tiny fixed program — usually two threads with a handful of memory operations — run many times to see which final states are observable. It answers a narrow yes/no question: can this outcome ever occur on this platform or under this implementation?

open as a page

What design seams does a codebase need so that time-dependent and asynchronous behaviour can be verified without sleeps, real timers, or background thread pools in the test run?

level: middleimportance: should knowfreq 50%

basics

~20 s

Make time and execution injected dependencies: pass in a clock and an executor/scheduler rather than calling the system clock or spawning threads inline, and give async operations an observable completion handle. Tests then substitute a fake clock and a same-thread or manually pumped scheduler.

open as a page

A concurrency bug reproduces in production but vanishes the moment you add logging around it or attach a debugger. Why does that happen, and how do you gather evidence without destroying the conditions you need?

level: middleimportance: should knowfreq 48%

basics

~20 s

Observation changes timing. Logging and breakpoints add delay and internal synchronisation that widen or close the racy window, so the interleaving that triggered the bug stops occurring. Collect evidence with low-overhead, always-on instruments instead: sampling, counters, in-memory ring buffers, and post-mortem dumps.

open as a page

How can a lock-order analysis tool report a potential deadlock from a run in which no deadlock actually occurred, and what does it need to observe to do that?

level: middleimportance: should knowfreq 36%

basics

~20 s

It records every nested lock acquisition as an edge "A acquired before B" in a global lock-order graph, usually keyed by lock class rather than instance. A cycle in that graph means two code paths take the same locks in opposite orders, which is a deadlock waiting for the right interleaving — no actual hang required.

open as a page

Explain how a vector-clock-based race detector tracks the happens-before relation: what state does it keep per thread and per memory location, and how is it updated as the program runs?

level: middleimportance: should knowfreq 40%

basics

~20 s

Each thread carries a vector clock: one counter per thread, its own bumped on synchronization. Releasing a lock copies the releaser's clock into the lock; acquiring joins that clock into the acquirer's. Each memory location stores the clock value of its last write and reads. An access races if the prior conflicting access's stamp is not covered by the current thread's vector clock.

open as a page

A stress test fails roughly once every few thousand runs. How do you turn that into a reproducible, minimal failing schedule you can actually debug?

level: middleimportance: should knowfreq 30%

basics

~20 s

First make it repeatable: derive all nondeterminism from a logged seed, or record and replay the schedule under a controlling scheduler. Then minimize by delta debugging — repeatedly drop operations, threads and delay points, keeping any reduction that still fails — until every remaining element is necessary. Keep the result as a permanent regression test.

open as a page

Some async frameworks let a test replace the real thread pool with a single-threaded scheduler whose queued tasks the test runs manually, one step at a time. What does that buy you, and which classes of concurrency bug can it still not catch?

level: seniorimportance: should knowfreq 42%

basics

~20 s

It makes the interleaving a test input: you choose the order tasks run, so a specific race is reproduced exactly and every run behaves identically. It cannot catch bugs that live below task granularity — memory-visibility and instruction-reordering effects, true parallel data races, or contention and performance problems.

open as a page

A service stops getting faster — or gets slower — as you add worker threads, while the machine's processors are far from saturated. How do you confirm that lock contention is the cause and identify the specific hot lock?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Profile where threads wait, not just where they run: use off-CPU or blocked-time profiling, or sample thread dumps and count how often threads sit blocked on the same lock. Rising context-switch rates and flat throughput with growing thread counts confirm contention; the top blocked-on stack names the lock.

open as a page

Compare happens-before race detection with lockset-based detection (the Eraser style, where a tool tracks which locks are consistently held): what does each report, and where does each go wrong?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Lockset detection tracks, per location, the intersection of locks held on every access; an empty intersection means no consistent lock, so it warns. It finds bugs the observed schedule hid, but false-alarms on correct code that uses ordering instead of locks. Happens-before detection reports only genuinely unordered pairs — precise, but blind to schedules it did not run. Hybrids combine both.

open as a page

What does it mean to model-check a concurrent algorithm, and why does state-space explosion dominate the discussion of the technique?

level: seniorimportance: should knowfreq 34%

basics

~20 s

You describe the system as states and transitions plus properties it must satisfy, then a checker exhaustively explores every reachable state and interleaving to prove the properties or produce a counterexample trace. The reachable state count grows exponentially in components and interleavings, so the entire practical craft is keeping the model small and pruning equivalent explorations.

open as a page

What is schedule fuzzing — running a concurrent test under a randomized or controlled scheduler instead of the operating system's — and what does it give you that ordinary stress testing does not?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The test runs on a scheduler you control: threads only advance when it says so, and it picks the next thread by a seeded random policy. That makes coverage of interleavings deliberate rather than accidental, makes any failure replayable from its seed, and lets you bias toward bug-prone schedules such as few well-placed preemptions.

open as a page

A continuous-integration suite contains asynchronous tests that fail about one run in a hundred, with no obvious pattern. How would you handle that as a team, and what would you do to each individual test?

level: principalimportance: should knowfreq 46%

basics

~20 s

Treat flakiness as a defect with an owner, not noise: detect and track it automatically, quarantine loudly with a deadline instead of blanket retries, then fix each test by removing the timing dependency — signals and virtual clocks instead of sleeps, isolated state, no ordering assumptions — and check whether the flake is actually a product bug.

open as a page

You are asked to make the next concurrency incident in a production service diagnosable in minutes rather than days. What would you build into the system ahead of time — including which pool and queue metrics — and what continuous overhead would you accept for it?

level: principalimportance: should knowfreq 40%

basics

~20 s

Pre-install evidence collection: per-pool metrics (queue depth and oldest-item age, wait time versus service time, active workers, rejections), always-on sampling profiling on and off processor, watchdog-triggered thread dumps stored durably, and a capture-before-restart rule. A few percent steady overhead is a fair price.

open as a page

Your concurrency tests run clean under a dynamic race detector. What can you legitimately conclude, and how would you build a strategy that covers what the detector structurally cannot see?

level: principalimportance: should knowfreq 28%

basics

~20 s

Only that no race appeared on the code paths, data and schedules that actually executed. Dynamic detection is bounded by code coverage, thread-interleaving coverage, and the classes of bug it models. Cover the gaps with schedule perturbation, static checking with ownership annotations, and design choices that make whole categories unrepresentable.

open as a page