Threads, races and locks · Operating Systems
Why do two threads adding to one counter get the wrong answer?
Course contentsWhy 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:
- read count into a register
- add 1
- 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 yourselfEach 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)- L14Both read 5 before either writes.
- L11A writes 6, then B writes 6 again.
- 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?
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)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.
- PredictCount the lost updates
Type exactly what this program prints.
- ModifyAdd the lock
run(schedule)replays steps of threads that each doread, add, writeon a shared counter. Add a lock: a thread may onlyreadif no other thread holds the lock, and it takes the lock onreadand releases it onwrite. When a step can't run, skip it and remember it, then retry the waiting steps after eachwrite. Return the final count. - 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
| Term | Chinese | Definition |
|---|---|---|
| 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. |