Skip to content

11/32 · 5 min

When two threads touch the same thing

Two threads each add one to a shared counter, a thousand times. The final value should be two thousand. It is usually less, and by a different amount each run.

Why

counter += 1 is not one operation. It is three:

code
load  counter into a register
add   1
store the register back

A switch between the load and the store loses an increment:

code
thread A: load  -> 41
thread B: load  -> 41
thread A: add, store -> 42
thread B: add, store -> 42     one increment gone

The result depends on exactly when the scheduler interrupts, which is why the bug is intermittent, disappears under a debugger, and never reproduces on the machine where you are looking for it.

The critical section

The stretch of code that touches shared state is the critical section, and correctness needs three properties:

Mutual exclusion. At most one thread inside at a time.

Progress. If nobody is inside, a thread wanting in must get in. A protocol where everyone politely waits forever satisfies mutual exclusion and is useless.

Bounded waiting. A thread cannot be passed over indefinitely while others go repeatedly.

What does not fix it

python
if not busy:        # check
    busy = True     # and set, two steps
    ...

Two threads can both pass the check before either sets the flag. Any fix built from ordinary reads and writes has this problem somewhere, which is why the hardware provides an instruction that tests and sets in one indivisible step, and every lock is built on it.

Atomic operations

For a counter specifically, an atomic increment is enough and is cheaper than a lock, because it is one instruction rather than a kernel call.

java
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();      // one indivisible operation

Recognising the risk

Ask of any shared variable: is there a point between reading it and writing it where another thread could interfere? If yes, and it is not protected, it is a race whether or not you have seen it fail.

Data that is never written after creation is safe by construction, which is the strongest argument for immutability: a value that cannot change cannot be raced.