Data Races
A data race is two unsynchronized accesses to the same memory where at least one is a write — undefined behavior in most language models, distinct from higher-level race conditions.
Definition
A data race is the lowest-level synchronization failure: conflicting memory accesses from different threads without an ordering relation between them. Languages classify it as undefined behavior (C++, Rust with unsafe) or bound it with weak guarantees (Java, Go). It differs from race conditions — logical timing bugs — which can exist even in race-free programs.
Context and scope
Detected with ThreadSanitizer, Java race detectors, and loom fuzzing. Explains why unlocked read-modify-write counters and unsynchronized publication corrupt state.
Common misconceptions
- Data races cause wrong values occasionally — undefined behavior is broader: register-cached reads, torn writes, and compiler assumptions can corrupt unrelated code
- Atomic types fix data races — correct synchronization fixes them; an atomic on the wrong variable leaves the race on the other one
- Races only matter on multi-core — single-core preemptive threading races through context switches and compiler reordering just the same
Anti-patterns
- Incrementing shared counters with read-modify-write instead of atomic operations
- Publishing object references before all fields are written and relying on luck
- Testing under low concurrency and shipping races that only interleave under production scheduling
Related concepts
Related examples
Authoritative source
What it is
Two accesses conflict when they target the same location and one writes. Without happens-before ordering between them, the program has a data race. The consequences range from stale reads to torn 64-bit values on 32-bit platforms to whole-program undefined behavior in strict languages.
How it works
Compilers cache values in registers, fuse and reorder accesses, and delete writes they can prove redundant under single-thread reasoning. CPUs add store buffering and relaxed visibility. Detectors work by shadow-tracking access orderings and flagging unordered conflicting pairs — ThreadSanitizer catches races that ran but did not yet corrupt anything.
When to hunt
Any shared mutable state touched from callbacks, timers, and multiple requests. Make sharing explicit — pass ownership, use channels, or guard with one lock — and run race detectors in CI where the runtime supports it.
Pitfalls
Races hide behind happens-lucky on strongly ordered x86. Fixing the wrong variable with atomic leaves the race intact on its twin field. Some fixes introduce deadlocks instead: synchronize one field and wait on another. High-level race conditions — check-then-act sequences — remain even after every data race is gone.