Interviewers often distinguish a 'data race' from a 'race condition'. Define each precisely, and show that neither one implies the other.
answer
- data race = accesses, race condition = invariants
- same location, one write, no happens-before
- locked check + locked act still races
- detectors find data races only
- DRF is necessary, not sufficient
basics
~20 sA data race is a memory-model property: two threads access the same location concurrently, at least one writes, with no ordering between them. A race condition is a correctness property: the result depends on timing. You can have a race condition with zero data races (all accesses properly synchronized, but the logic assumes state cannot change between steps) and a data race that never produces a wrong-looking answer.
solid answer
~60 s**Data race** — a precise, mechanical definition from the memory model: two threads access the same memory location, at least one access is a write, the accesses are not ordered by any synchronization (no happens-before edge between them), and neither is a specially marked atomic. It is detectable by tools and, under most memory models, it makes the program's behaviour undefined. **Race condition** — a design-level defect: the correctness of the outcome depends on the relative timing of operations. This is about your invariants, not about memory. They are independent: - **Race condition, no data race.** Two threads each take a lock, check a balance, release, then take the lock again and withdraw. Every access is synchronized, no data race exists — and the account still overdraws because the state changed between the check and the act. The atomic unit was drawn too small. - **Data race, no visible misbehaviour.** An unsynchronized flag or statistics counter may produce plausible values on your hardware today. It is still a data race, still undefined behaviour, and the compiler is entitled to break it later. The practical consequence: race detectors find data races; only reasoning about invariants finds race conditions.
code
text · 8 lines# both threads see balance = 100, both withdraw 80
T1: lock; b=100; unlock
T2: lock; b=100; unlock
T1: if 100>=80: lock; balance = 100-80 = 20; unlock
T2: if 100>=80: lock; balance = 20-80 = -60; unlock
# no data race exists: every access is under the mutex
# the atomic unit was too small: check and act must be onego deeper
State the two definitions and give one example of each: unsynchronized shared counter (data race) versus check-then-act with a lock on each half (race condition).
Show that neither implies the other with concrete code, and explain that data races are a memory-model property while race conditions are about invariants.
Draw the operational conclusion: detectors cover one class only, and the other requires identifying the invariant and widening the atomic boundary. Note that data-race freedom is necessary but not sufficient.
Discuss the distinction as a design contract — which state has a defined owner and atomic unit, how that is enforced across process and service boundaries, and what tooling can and cannot verify.
## Two definitions that people blur together The words sound like synonyms; they are not, and the distinction matters because the two are found by different means and fixed at different levels. ### Data race — a memory-model term A program has a data race when all of these hold simultaneously: 1. two or more threads access the **same memory location**; 2. at least one of those accesses is a **write**; 3. the accesses are **not ordered by synchronization** — there is no happens-before relationship established by a lock, an atomic operation, a thread start/join, a channel send/receive, or similar; 4. the accesses are **not marked as atomic** in the language's memory model. That definition is entirely mechanical. It says nothing about whether the program produces the wrong answer; it is a property of the *accesses*, and a tool can check it. It is also the trigger for the memory model's escape clause: in most models a program containing a data race has undefined (or at minimum, wildly unconstrained) behaviour, and the guarantee you normally rely on — that a data-race-free program behaves as some interleaving of its threads' statements — no longer applies. ### Race condition — a correctness term A race condition exists when the correctness of the outcome depends on the relative timing or interleaving of operations. It is a statement about your **invariants**: some sequence of steps needed to appear indivisible, and it did not. This is a design-level property. It can exist at any granularity — between two instructions, between two service calls, between two database statements, between a filesystem check and a filesystem open. It exists in systems with no shared memory at all. ## Neither implies the other ### Race condition without a data race ``` withdraw(amount): lock(m); bal = balance; unlock(m) # synchronized read if bal >= amount: lock(m); balance = balance - amount; unlock(m) # synchronized write ``` Every access to `balance` is protected. A race detector reports nothing: there is no data race, because every pair of conflicting accesses is ordered by the mutex. The code is still broken — two threads can both read a balance of 100, both pass the check for 80, and both withdraw, leaving −60. The bug is that the atomic unit should have spanned the check *and* the act, and instead it covered each half separately. The same shape appears without any shared memory: check with a service that a username is free, then create the account; two clients pass the check and one creation fails or duplicates. No memory is shared between the machines at all, so "data race" is not even a meaningful term — but the race condition is real. ### Data race without observable misbehaviour An unsynchronized `hitCount++` used only for a rough log line, or an unsynchronized `stopRequested` flag, will typically produce values that look fine. It is nonetheless a data race by definition. The reason it still matters is that the memory model, not your test run, decides what is allowed: the compiler may hoist the flag read out of a loop (turning a terminating loop into an infinite one), keep the value in a register indefinitely, or transform the code in ways that only make sense under the assumption that no other thread touches that location. "It works" today is a statement about one compiler version on one machine. ## Why the distinction is operationally useful - **Different detection.** Dynamic race detectors (happens-before/lockset based) find data races and are very good at it. They will not tell you that your check-then-act sequence needed a wider lock — that requires knowing the invariant, which no tool knows. - **Different fixes.** Data races are fixed by adding synchronization or atomics to the accesses, or by not sharing. Race conditions are fixed by widening the atomic unit — one lock around the whole compound action, a conditional/compare-and-swap update, a transaction, a unique constraint, or moving the state under a single owner. - **Different reviewers' questions.** For a data race: "which lock or ordering edge covers this location?" For a race condition: "what must be true when this executes, and can another thread falsify it between these two lines?" ## The one-sentence version A data race is about **unsynchronized access to memory**; a race condition is about **timing-dependent correctness**. Removing all data races is necessary for a shared-memory program to be well-defined, but it is nowhere near sufficient for it to be correct.
- Your race detector reports a clean run on a service that still produces duplicate records under load. What does that tell you, and where do you look?It tells you the accesses are properly synchronized, so the defect is a race condition rather than a data race — the atomic unit is drawn too narrowly somewhere. Look for check-then-act sequences: an existence check followed by an insert, a read-validate-write across two transactions, a cache lookup followed by a populate. The fix widens the unit — a single conditional/atomic operation, one lock or transaction spanning check and act, or a uniqueness constraint that makes the database arbitrate.
- Can a race condition exist in a program with no shared memory whatsoever?Yes, and this is the clearest proof the two concepts are distinct. Two clients that ask a service "is this name taken?" and then create the account both act on an answer that became stale in flight. Nothing shares memory, so no data race is even definable, yet the outcome depends on timing. The fix is the same in spirit: make the check and the act one atomic operation at the authority that owns the state.
saying these in an interview costs you the question
- Using the two terms interchangeably, or defining a data race as 'when the output is wrong'
- Claiming that a clean race-detector run means the concurrent code is correct
- Believing that if every shared field is individually synchronized the code has no races
- Saying a data race is harmless because the observed values look sane
- Thinking race conditions require shared memory