The mutex
The classic fix for a race is a lock, also called a mutex, short for mutual exclusion. A lock is a token only one thread can hold.
- A thread acquires the lock before touching the shared data, and 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, closing exactly the gap that lesson 5-2 exploited.
| Python | Meaning |
|---|---|
lock = threading.Lock() | create the token |
with lock: | acquire, run the block, release |
The with statement guarantees release even if the code inside raises an exception, which is why it is preferred over manual acquire and release calls. A lock left held by a crashed thread would stop every other thread forever.
The same four threads, now correct
Every increment happens inside the lock, and the answer is exact on every run.
import threading counter = 0 lock = threading.Lock() def worker(): global counter for _ in range(100_000): with lock: counter += 1 threads = [threading.Thread(target=worker) for _ in range(4)] for t in threads: t.start() for t in threads: t.join() print(counter)
Output
400000with lock: acquires before the block and releases after, so read-add-write can never be interleaved. Four threads times 100,000 increments gives exactly 400000, every single run.
The determinism is the whole gain. Compare it against the previous lesson, where the identical loop without the lock produced a different number each time and no amount of rerunning made it trustworthy.
Protecting a shared balance
Two threads each deposit 1 into a shared balance 100,000 times.
import threading balance = 0 lock = threading.Lock() def deposit_many(): global balance for _ in range(100_000): with lock: balance += 1 t1 = threading.Thread(target=deposit_many) t2 = threading.Thread(target=deposit_many) t1.start() t2.start() t1.join() t2.join() print(balance)
Output
200000balance += 1 is indented under with lock:, which is what makes the increment atomic.
The lock must be the same lock object for both threads, and it is here because both use the global lock. Creating a lock inside deposit_many would give each thread its own token, and the code would look protected while providing no protection whatsoever.
The costs of locking
Locks are not free, and they introduce failure modes of their own.
Less parallelism. Threads queue at the lock, so the protected part runs one at a time. Keep critical sections small, because a lock held across a slow network call serializes the entire program.
Deadlock. Thread A holds lock 1 and waits for lock 2 while thread B holds lock 2 and waits for lock 1, and both wait forever. The standard defense is to always acquire multiple locks in the same fixed order, and the next lesson makes one happen live and then fixes it.
Forgetting the lock somewhere. One unprotected access reopens the race. The discipline is that every touch of the shared data goes through the same lock, with no exceptions for code that looks harmless.
| Cost | Mitigation |
|---|---|
| serialized critical section | keep it short |
| deadlock | one global lock order |
| a forgotten access | wrap the data in one class that owns the lock |
Databases, git, and the OS's own process table all rely on this exact idea with fancier machinery on top.
Two locks, two threads, opposite orders
Thread A holding the database lock and waiting for the log-file lock, while thread B holds the log-file lock and waits for the database lock, is a deadlock. The classic prevention is to always acquire locks in the same agreed order.
Each thread is waiting for the other, forever, and neither will release what it holds while it waits.
| Rule | Effect |
|---|---|
| any order allowed | a circular wait can form |
| database always before log | the circle cannot form |
If every thread must acquire database and then log in that order, the circular wait becomes structurally impossible. Lock ordering is also a favorite interview question, so it is worth being able to state in one sentence.