Race condition

On this page 7

A race condition is a defect in which the correctness of a program depends on the relative timing or interleaving of operations that run concurrently, such as threads, processes or asynchronous tasks. In concurrency, it arises when two or more flows of execution access shared state and at least one of them modifies it without adequate coordination.

Race conditions are notorious because they are nondeterministic: the same code may run correctly thousands of times and then fail under a slightly different load, scheduler decision or hardware configuration. They cause lost updates, corrupted data, crashes and security vulnerabilities, and they are among the hardest defects in programming to reproduce.

How a race condition occurs

Most race conditions come from operations that look atomic in source code but are several steps at the machine level. The statement counter = counter + 1 typically compiles into a read, an addition and a write. If two threads both read the value 41 before either writes, both write 42, and one increment is lost.

The section of code that touches shared state and must not be interleaved is called a critical section. A race occurs when a critical section is not protected, so the scheduler can switch between threads at any instruction boundary, or threads on different cores can run the steps truly in parallel.

Modern hardware adds another layer. Compilers and processors reorder memory operations for performance, and each core may see writes from other cores late. Without proper synchronization, one thread can observe a partially initialized object, which is why language memory models define happens-before relationships that only synchronization creates.

Data races and other forms of race conditions

The terms data race and race condition are related but not identical. A data race is a precise low-level condition defined by language memory models; a race condition is the broader logical defect. The table shows the main forms.

FormDefinitionTypical consequence
Data raceUnsynchronized concurrent accesses to one memory location, at least one a writeUndefined behavior in C and C++; torn or stale values
Read-modify-write raceTwo flows read, change and write the same valueLost updates, wrong counters
Check-then-act raceA condition is checked and acted on in separate stepsDuplicate records, double spending
TOCTOU raceTime of check differs from time of use for a resourceSecurity holes in file access
Order violationAn operation runs before the one it depends onUse of uninitialized data
Async interleaving raceSingle-threaded tasks interleave at await pointsStale state in user interfaces and servers
Main forms of race conditions

A program can be free of data races and still contain race conditions. Two transactions that each lock correctly but check and then reserve the last seat in separate locked steps can still sell the seat twice. Conversely, asynchronous programming in a single-threaded event loop has no data races, yet it can race at every await point.

Techniques that prevent race conditions

Prevention means either removing shared mutable state or making every access to it coordinated. The right technique depends on contention, performance requirements and how much state is shared.

TechniqueHow it prevents racesTrade-off
Mutex or lockOnly one thread enters the critical sectionContention and risk of deadlock
Atomic operationsHardware performs read-modify-write as one stepWorks only for simple values
Compare-and-swap loopsRetry an update until no one interferedComplex; possible livelock under contention
Immutable dataShared values never changeCopies cost memory and time
Message passingEach piece of state has one ownerDesign shift toward actors or channels
Database transactionsIsolation levels and row locks serialize accessLower throughput at strict levels
Prevention techniques and their trade-offs

Locks solve races but introduce their own hazard: if two threads acquire two locks in opposite order, they can wait for each other forever, which is a deadlock. Acquiring locks in a consistent global order and keeping critical sections short are the standard defenses.

Detecting and diagnosing race conditions

Because races depend on timing, ordinary testing finds them only by chance. Effective detection combines specialized tools with design reviews.

MethodWhat it findsLimitation
Dynamic race detectorsData races observed at runtime, such as with ThreadSanitizer or the Go race detectorOnly paths that actually execute; slows programs several times
Static analysisUnprotected shared accesses in codeFalse positives and missed cases
Stress testingFailures under high concurrency and repetitionNondeterministic results
Deterministic schedulersExplores many interleavings systematicallyLimited to small components
Type systemsRust ownership rules forbid data races at compile timeLogical races remain possible
Tools and methods for finding race conditions

Symptoms that should raise suspicion of a race include failures that disappear when logging is added, tests that fail only on build servers with many cores, and values that are almost but not quite correct, such as a total that is occasionally a few units short.

Common misconceptions

  • A single-core machine cannot have races: preemptive scheduling interleaves threads on one core as well.
  • The volatile keyword makes code thread-safe: in Java it guarantees visibility, and in C and C++ it does not provide synchronization at all; neither makes compound operations atomic.
  • Rare failures are acceptable: a race that fails once in a million operations fails many times a day at scale.
  • Adding a sleep fixes it: delays only change probabilities and hide the defect.
  • Thread-safe collections make code safe: two individually safe calls in sequence still form a check-then-act race.

The lasting lesson is that correctness must not depend on timing. Code is correct only when every possible interleaving produces an acceptable result, and parallelism amplifies any race that a design leaves open.

Put simply

A race condition happens when two parts of a program try to change the same thing at the same moment, and the result depends on which one happens to be faster. Most of the time things work, but sometimes the timing lines up badly and one change silently overwrites the other.

Picture two people editing the same shopping list from separate copies. Both see “milk”, one adds bread, the other adds eggs, and each saves their own copy. Whichever copy is saved last wins, and one item quietly disappears. Programs need rules, like taking turns, to stop this from happening.

In practice

An online event shop sells limited seats. The purchase handler reads the remaining seat count, checks that it is above zero, charges the customer and then decreases the count. During normal days this works, but when a popular show opens with several hundred buyers in the first minute, the shop sells more seats than the hall holds.

Investigation shows a check-then-act race: several requests read a count of 1 at the same time, all pass the check and all complete the sale. Load tests on a developer laptop never reproduced it because requests rarely overlapped there.

The team replaces the separate read and write with one atomic conditional update in the database, which decreases the count only if it is still above zero and reports whether a row changed. Overselling stops completely, and a stress test with 1,000 concurrent purchase attempts for 100 seats now confirms exactly 100 successful sales.