Guide

Race conditions

Why threads that share data break.

Threads share data

Threads in one process share its memory, like the heap and global values. So if one thread changes a shared counter, every other thread sees the new value. Each thread still has its own stack for its local values. Separate processes don't share memory at all.

Race conditions

Adding 1 to a counter looks like one step, but the CPU does three: load the value into a register, add 1, and store it back. Another thread can run between any two of these steps. If two threads both load 5 before either stores, both store 6, and one update is lost. When the result depends on the timing of threads like this, it is a race condition.

Critical sections

Code that touches shared data is a critical section. Only one thread at a time may be inside it. This rule is called mutual exclusion. Then one thread's steps finish before the next starts.

Atomic operations

An atomic operation happens all at once. No other thread can see it half done or slip in between its steps. Hardware gives a few small atomic steps, such as an atomic add for a counter. A bigger change, like moving money between two accounts, takes many steps, so it still needs a critical section.

Bugs that hide

A race only goes wrong when threads switch at just the wrong moment. That may happen once in a million runs, so racy code can pass every test. Adding logging changes the timing, so the bug often vanishes when you look for it. Such a bug is called a heisenbug. So protect shared data by design.

Remember

  • Threads share memory; each has its own stack.
  • Adding 1 is load, add, store, and threads can interleave them.
  • Only one thread at a time in a critical section.
  • Atomic means all at once.
  • Passing tests don't prove there is no race.