Race conditions
A data race is two threads touching the same memory with no ordering between
them and at least one writing. The symptom is nondeterminism: it passes a
thousand times, then corrupts state once. It hides because a debugger, a log
line, or a single-core test all perturb the timing that triggers it. You
cannot patch what you cannot see, so first make it reproducible.
Method
- Name the shared mutable state and the unsynchronized access. A race
needs three things: shared data, concurrent access, and no
happens-before edge between the accesses. List every thread that reads or
writes the suspect variable and which lock, if any, each holds. A field
written under a lock but read without one is a race even though a lock
exists.
- Run a sanitizer instead of guessing. ThreadSanitizer (
-fsanitize= thread in Clang/GCC, go test -race, -race in Rust nightly) reports
the two conflicting accesses with both stacks and the missing
synchronization. Java has the Java Concurrency Stress tests (jcstress);
.NET has the concurrency analyzers. This finds races that never crashed
in your runs.
- Reason about happens-before, not about "unlikely". Two accesses are
safe only if an ordering edge connects them: a lock released then
acquired, a thread start or join, a channel send then receive, an atomic
with acquire/release. If you cannot draw that edge, the timing that breaks
it will eventually happen. "The window is tiny" is not synchronization.
- Stress the timing to force the failure on demand. Wrap the suspect
operation in a loop of thousands of iterations across more threads than
cores, add randomized
sched_yield/Thread.sleep(0) at the
read-modify-write seam, and pin to fewer cores to widen the window. A
test that fails 1 in 10 now instead of 1 in a million is a test you can
debug.
- Fix by establishing ordering, not by narrowing the window. Put every
access to the shared state under the same lock, replace a
read-modify-write with an atomic compare-and-swap, make the data
immutable after publication, or hand ownership to one thread. Adding a
sleep or a second check narrows the race, it does not close it.
- Re-run the stress loop and the sanitizer clean. The stress harness
from step 4 must survive a long run, and the sanitizer must report zero
races. A green sanitizer with a still-flaky stress test means a second
race or a higher-level atomicity bug: repeat from step 1.
Signals
- Does the failure rate rise with more threads and fall with fewer cores?
- Can you draw a happens-before edge for every access to the shared field?
- Does ThreadSanitizer report zero races after the fix, not just fewer?
Boundaries
Sanitizers catch data races on memory, not higher-level atomicity or logic
races (check-then-act on a database row, a TOCTOU on the filesystem); those
need transactions or locks at the resource, and the deadlock-analysis skill
covers the locks you add here overreaching into mutual blocking.
1---2name: race-conditions3description: Diagnose data races by reasoning about happens-before order, running sanitizers, and stressing the timing until the bug shows. Use when a bug appears only under load, only sometimes, or vanishes when you add a print statement.4---56# Race conditions78A data race is two threads touching the same memory with no ordering between9them and at least one writing. The symptom is nondeterminism: it passes a10thousand times, then corrupts state once. It hides because a debugger, a log11line, or a single-core test all perturb the timing that triggers it. You12cannot patch what you cannot see, so first make it reproducible.1314## Method15161. **Name the shared mutable state and the unsynchronized access.** A race17 needs three things: shared data, concurrent access, and no18 happens-before edge between the accesses. List every thread that reads or19 writes the suspect variable and which lock, if any, each holds. A field20 written under a lock but read without one is a race even though a lock21 exists.222. **Run a sanitizer instead of guessing.** ThreadSanitizer (`-fsanitize=23 thread` in Clang/GCC, `go test -race`, `-race` in Rust nightly) reports24 the two conflicting accesses with both stacks and the missing25 synchronization. Java has the Java Concurrency Stress tests (`jcstress`);26 .NET has the concurrency analyzers. This finds races that never crashed27 in your runs.283. **Reason about happens-before, not about "unlikely".** Two accesses are29 safe only if an ordering edge connects them: a lock released then30 acquired, a thread start or join, a channel send then receive, an atomic31 with acquire/release. If you cannot draw that edge, the timing that breaks32 it will eventually happen. "The window is tiny" is not synchronization.334. **Stress the timing to force the failure on demand.** Wrap the suspect34 operation in a loop of thousands of iterations across more threads than35 cores, add randomized `sched_yield`/`Thread.sleep(0)` at the36 read-modify-write seam, and pin to fewer cores to widen the window. A37 test that fails 1 in 10 now instead of 1 in a million is a test you can38 debug.395. **Fix by establishing ordering, not by narrowing the window.** Put every40 access to the shared state under the same lock, replace a41 read-modify-write with an atomic compare-and-swap, make the data42 immutable after publication, or hand ownership to one thread. Adding a43 sleep or a second check narrows the race, it does not close it.446. **Re-run the stress loop and the sanitizer clean.** The stress harness45 from step 4 must survive a long run, and the sanitizer must report zero46 races. A green sanitizer with a still-flaky stress test means a second47 race or a higher-level atomicity bug: repeat from step 1.4849## Signals5051- Does the failure rate rise with more threads and fall with fewer cores?52- Can you draw a happens-before edge for every access to the shared field?53- Does ThreadSanitizer report zero races after the fix, not just fewer?5455## Boundaries5657Sanitizers catch data races on memory, not higher-level atomicity or logic58races (check-then-act on a database row, a TOCTOU on the filesystem); those59need transactions or locks at the resource, and the deadlock-analysis skill60covers the locks you add here overreaching into mutual blocking.