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:

  1. t2 loads the value of counter, which is 0.
  2. t2 executes the loop for N-1 iterations, each time loading, incrementing, and storing. After N-1 iterations, counter = N-1.
  3. t1 doesn’t know that counter changed. It still holds the old value 0 from its initial load. It now stores 1 to counter, overwriting N-1.
  4. t2 loads the value of counter, which is now 1 (the value t1 just wrote).
  5. t1 executes the remaining N-1 iterations, incrementing counter from 1 up to N.
  6. t2 doesn’t know that counter changed. It still holds the stale value 1. It now stores 2 to counter, overwriting N.

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 counter is 2N—achieved when there is no harmful interleaving.
  • The minimum value of counter when N > 1 is 2, achieved through the adversarial schedule described above.
  • ++counter is 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++