Data Races: Maximum and Minimum Possible Values
April 23, 2026 · Luciano Muratore
Data Races: Maximum and Minimum Possible Values
In multithreading, a data race occurs when two or more threads access a shared variable concurrently, at least one of them writes to it, and there is no synchronization in place.
The result is undefined behavior, but it is still worth asking: what are the bounds? Given a race, what is the best and the worst the program can produce?
The Setup
Consider the following code:
#include <iostream>
#include <thread>
int counter = 0; // shared variable (NOT atomic)
void increment(int N) {
for (int i = 0; i < N; N++) {
++counter; // NOT Thread-Safe (data race)
}
}
int main() {
int N = 100;
std::thread t1(increment, N);
std::thread t2(increment, N);
t1.join();
t2.join();
std::cout << "Final counter = " << counter << std::endl;
}
Two threads, t1 and t2, each run increment with N = 100. If everything went perfectly, every increment atomic, no interleaving, the final value would be 2N. But ++counter is not atomic. It compiles down to three operations: load, add, store. And that gap is where races live.
The Maximum Possible Value
This one is easy. If the two threads never interfere with each other, one runs to completion before the other starts, or they happen to interleave in a perfectly cooperative way, every increment lands. The result is 2N.
This is the ideal case, and it is also the upper bound.
The Minimum Possible Value
This is the more interesting question. How bad can it get?
When N > 1, the minimum possible value of counter is 2.
At first, this feels wrong. How can two threads each looping N times produce a final count of just 2? The answer lies in a specific, adversarial thread schedule.
The Worst-Case Schedule
Here is the exact sequence of events that produces 2:
t2loads the value ofcounter, which is0.t2executes the loop forN-1iterations, each time loading, incrementing, and storing. AfterN-1iterations,counter = N-1.t1doesn’t know thatcounterchanged. It still holds the old value0from its initial load. It now stores1tocounter, overwritingN-1.t2loads the value ofcounter, which is now1(the valuet1just wrote).t1executes the remainingN-1iterations, incrementingcounterfrom1up toN.t2doesn’t know thatcounterchanged. It still holds the stale value1. It now stores2tocounter, overwritingN.
The final value of counter is 2.
This is precisely what Figure 20.1 illustrates: both threads complete all N iterations, yet the result is catastrophically wrong.
Why This Happens
The root cause is that ++counter is not one operation—it is three:
load → read the current value into a register
add → increment the register
store → write the register back to memory
Between the load and the store, the other thread is free to run. If it writes to counter in that window, the first thread’s store will silently overwrite it. Neither thread sees the other’s work.
This is not a bug in any one thread. Both threads execute their loops correctly. The race is in the gap between reading and writing, and it is invisible at the source code level.
Summary
- The maximum value of
counteris2N—achieved when there is no harmful interleaving. - The minimum value of
counterwhenN > 1is2, achieved through the adversarial schedule described above. ++counteris not atomic: it is a load-add-store sequence, and any of those steps can be interrupted.- A data race does not just produce random noise. It produces values within bounds that can be reasoned about, which makes understanding the worst case both possible and worth doing.
- Final Insight: A data race is not chaos. It is a gap, a window between reading and writing, and the minimum value tells you exactly how wide that gap can be exploited.
Reference: Elements of Programming Interviews in C++