The mutex
The classic fix for a race is a lock (also called a mutex, for mutual exclusion). A lock is a token only one thread can hold:
- A thread acquires the lock before touching the shared data. If another thread holds it, this thread waits.
- It does the read-modify-write.
- It releases the lock, waking one waiter.
The code between acquire and release is called the critical section. Locks turn "three interruptible steps" into "one uninterruptible unit", exactly closing the gap you exploited in lesson 5-2.
In Python: lock = threading.Lock(), then with lock: around the critical section. The with statement guarantees release even if the code inside raises an exception.
Code exercise · python
Run this. Same four threads as the racy version in lesson 5-2, but now every increment happens inside the lock. The answer is exactly 400000, every single run.
Code exercise · python
Your turn. Two threads each deposit 1 into a shared balance 100,000 times. Protect the increment with the lock so the final balance is exactly 200000.
The costs of locking
Locks are not free, and they introduce their own failure modes:
- Less parallelism: threads queue at the lock, so the protected part runs one-at-a-time. Keep critical sections small.
- Deadlock: thread A holds lock 1 and waits for lock 2, thread B holds lock 2 and waits for lock 1. Both wait forever. The standard defense is to always acquire multiple locks in the same fixed order — the next lesson makes one happen live and then fixes it.
- Forgetting the lock somewhere: one unprotected access reopens the race. Discipline: every touch of the shared data goes through the same lock.
Databases, git, and even your OS's process table all rely on this exact idea, just with fancier machinery.
Quiz
Thread A holds the database lock and waits for the log-file lock. Thread B holds the log-file lock and waits for the database lock. What happened and what is the classic prevention?