Race conditions

1 min read

A race condition is when the outcome depends on the timing or order of threads accessing shared data. It is a bug in the code, but one that shows up only on some runs.

Read, modify, write

counter++ looks like one step and is three: read the value, add 1, write it back. If two threads both read 5, both write 6, and one increment is lost.

Two threads both read the counter as 5 and both write 6, so one increment is lost. THREAD A THREAD B COUNTER read 5 write 6 read 5 write 6 5 5 6 6, expected 7
A lost update. Both threads read the same value before either wrote.
C#
int counter = 0;
Parallel.For(0, 100_000, _ => counter++); // usually ends below 100000

// fixed:
int safe = 0;
Parallel.For(0, 100_000, _ => Interlocked.Increment(ref safe)); // always 100000

Check, then act

A check followed by an action is a race when another thread can change things in between. It stays a race on a ConcurrentDictionary, because each call is safe on its own but the pair is not.

C#
// Race: another thread can add the key between the check and the write
if (!cache.ContainsKey(key))
    cache[key] = Load(key);

// Atomic: one call
var value = cache.GetOrAdd(key, k => Load(k));

Fixes

Make the operation atomic: Interlocked.Increment for a counter, or the single-call methods of the concurrent collections, such as GetOrAdd and AddOrUpdate. Or guard the shared state with a lock (see synchronization primitivesSynchronization primitives and concurrent collectionsWhat to use when several threads touch the same data.Opens in a new tab). Or do not share it: give each worker its own copy and combine the results at the end, as in fan-out and fan-inFan-out and fan-in with Parallel.ForRender independent items on all cores, keep the results in order, then merge them one at a time.Opens in a new tab.

Where it matters

Races depend on timing, so they pass tests and fail under load. The other ways concurrent code fails are in deadlocksDeadlocksTwo or more threads wait on each other forever.Opens in a new tab and livelock and starvationLivelock and starvationThreads that keep running without progress, and threads that never get a turn.Opens in a new tab.


Sources