Race Conditions
A race condition arises when the result of concurrent operations depends on their timing, producing intermittent, hard-to-reproduce bugs.
When timing decides the answer
A race condition occurs when two or more threads access shared data concurrently, at least one writes, and the outcome depends on the order in which their operations happen to interleave. Because that order varies from run to run, the bug appears intermittently and often vanishes under a debugger, making races among the hardest defects to find.
The classic example
Consider two threads each doing counter = counter + 1. This is really three steps: read counter, add one, write it back. If both read the same value before either writes, one increment is lost. The final count is nondeterministic, sometimes correct, sometimes short, depending purely on timing.
Preventing races
- Mutual exclusion: a lock ensures only one thread enters a critical section
- Atomic operations: hardware-guaranteed indivisible read-modify-write
- Reductions: each thread accumulates privately, results combined safely at the end
- Immutability: data that is never written cannot race
The cost of protection
Synchronization prevents races but serializes access, so heavily contended locks become bottlenecks. The best defense is design: minimize shared mutable state, give each thread its own working data, and combine results only at coordination points. This avoids both the bug and the contention it invites.
Detection
Dynamic race detectors (such as ThreadSanitizer) instrument a program to flag conflicting unsynchronized accesses, and are far more reliable than testing alone, since a race may not manifest in most runs. In distributed code the analogue is a message-ordering bug; disciplined use of synchronization and clear ownership of data prevents both.