Locks and Mutex
A lock (mutex) is the basic tool for making a read-modify-write sequence atomic: only one thread can hold the lock at a time, and every other thread that tries to acquire it blocks until it is released. The fix for the broken counter from the previous chapter is a three-line change.
Fixing the Counter
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock: # only one thread inside at a time
counter += 1
threads = [threading.Thread(target=increment) for _ in range(4)]
[t.start() for t in threads]
[t.join() for t in threads]
print(counter) # always 400000 now
acquire and release
The with form is preferred because it releases the lock automatically even when the block raises. The manual form is useful when the critical section spans function calls:
lock.acquire()
try:
update_state()
finally:
lock.release()
Two extra variants exist on a lock: acquire(blocking=False) returns a boolean instead of waiting, and acquire(timeout=2) waits at most two seconds. Both are the basis of lock-free fallbacks: "try to take the lock, and if it is busy, do something else."
Reentrant Lock (RLock)
A Lock can only be acquired once per thread; a thread that tries to acquire it a second time deadlocks itself. An RLock (reentrant lock) may be acquired multiple times by the same thread, and is released only after that many releases. Use an RLock when a locked method calls another method that also takes the lock:
class BankAccount:
def __init__(self, balance=0):
self._balance = balance
self._lock = threading.RLock()
def deposit(self, amount):
with self._lock:
self._balance += amount
def transfer(self, other, amount):
with self._lock: # re-acquired by the same thread
with other._lock:
self._balance -= amount
other.deposit(amount) # fine: RLock allows re-entry
Scope Rules
- Hold locks for the shortest correct region - never around I/O,
time.sleep, or other threads'join(). - Use one lock per piece of state, and document which lock protects which object.
- Never create a lock inside a function that runs per call; the lock must outlive the critical sections.
Summary
Protect every shared read-modify-write with a threading.Lock used in a with block, prefer non-blocking or timed acquisition when a fallback exists, and reach for RLock only when the same thread must re-enter its own lock.