Everyone Can Learn Program
中文 Start learning
Core · Operating systemsLesson 2 of 6Free to read

Threads, races and locks · Operating Systems

Why do two threads adding to one counter get the wrong answer?

Course contents

Why you need it

Threads share memory, which makes them fast to coordinate and easy to get wrong. A race condition gives a wrong total once in a thousand runs and vanishes when you add a print to debug it. Seeing exactly how it happens, step by step, is the first defence; locks are the fix.

The intuition

count += 1 looks like one step but is three:

  1. read count into a register
  2. add 1
  3. write it back

If thread A reads 5, then thread B reads 5, both write 6. One increment is lost.

A lock (mutex) makes the three steps a critical section: only one thread may be inside at a time. The other waits at the door.

This lesson simulates threads by interleaving their steps, so you can see every possible order.

Definition

Race condition

A bug where the result depends on the timing of threads: which one runs which step first.

Lock (mutex)

Only one thread can hold it at a time. Code between acquire and release runs without interference from other threads that use the same lock.

Examples

Lose an update

Try it yourself

Each thread's increment is three steps. This schedule interleaves them badly.

count = 5
regs = {}

def step(thread, op):
    global count
    if op == "read":
        regs[thread] = count
    elif op == "add":
        regs[thread] += 1
    else:
        count = regs[thread]
    print(thread, op, "count =", count)

for thread, op in [("A", "read"), ("B", "read"), ("A", "add"), ("B", "add"), ("A", "write"), ("B", "write")]:
    step(thread, op)
Step by step
  1. L14Both read 5 before either writes.
  2. L11A writes 6, then B writes 6 again.
  3. L4Two increments, but count only went from 5 to 6.B write count = 6

Common mistakes

"It worked when I tested it"

Races depend on timing. A test that passes 100 times proves little; reason about the possible orders instead.

Locking in different orders

Thread 1 takes lock A then B, thread 2 takes B then A: each can end up waiting for the other forever. That's a deadlock (lesson 5).

Quick quiz

1. Two threads each run `count += 1` once, starting from 0, with no lock. Which results are possible?

  1. A1 or 2
  2. BAlways 2
  3. C0, 1 or 2
Show the answer

Answer: A

If the steps interleave, one update is lost (1); otherwise 2. Both write at least once, so never 0.

2. What does this print?

count = 0
for thread in ["A", "B"]:
    r = count
    r += 1
    count = r
print(count)
  1. A
    2
  2. B
    1
  3. C
    0
Show the answer

Answer: A

Here the threads run one after the other, never interleaved, so nothing is lost.

Practice

3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.

  1. PredictCount the lost updates

    Type exactly what this program prints.

  2. ModifyAdd the lock

    run(schedule) replays steps of threads that each do read, add, write on a shared counter. Add a lock: a thread may only read if no other thread holds the lock, and it takes the lock on read and releases it on write. When a step can't run, skip it and remember it, then retry the waiting steps after each write. Return the final count.

  3. WriteCan it go wrong?

    Write lost_updates(schedule) that replays a schedule of "T read", "T add", "T write" steps (no lock) on a counter starting at 0, and returns how many increments were lost: (number of writes) − (final count).

Practise this lesson

Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.

Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.

Terms in this lesson

TermChineseDefinition
Thread线程A sequence of execution inside a process; threads share the process's memory.
Race condition竞态条件A bug where the result depends on thread timing.
Mutex (lock)互斥锁Lets only one thread at a time into a critical section.
Critical section临界区Code that must not run in two threads at once.