Python Thread Synchronization
Thread synchronization is the set of tools that keep shared data safe when threads run together.
Force a lost update
Both threads read balance before the barrier lets either write. One deposit is lost every time.
import threading
balance = 0
barrier = threading.Barrier(2)
def deposit():
global balance
seen = balance
barrier.wait()
balance = seen + 100
threads = [threading.Thread(target=deposit) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(balance)Fix it with a Lock
The lock lets one thread finish its read and write. The second thread then reads the new value.
import threading
balance = 0
lock = threading.Lock()
def deposit_safe():
global balance
with lock:
seen = balance
balance = seen + 100
threads = [threading.Thread(target=deposit_safe) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(balance)The with lock idiom
The with block takes the lock at the top. It releases the lock at the end.
import threading
lock = threading.Lock()
with lock:
print("inside")
print("outside")Take the same lock twice with RLock
A helper may run inside code that already holds the lock. RLock lets the same thread take it again.
import threading
lock = threading.RLock()
with lock:
with lock:
print("nested ok")Example
import threading
balance = 0
barrier = threading.Barrier(2)
lock = threading.Lock()
def deposit_slow():
global balance
seen = balance
barrier.wait()
balance = seen + 100
def deposit_safe():
global balance
with lock:
seen = balance
balance = seen + 100
threads = [threading.Thread(target=deposit_slow) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Without lock: {balance}")
balance = 0
threads = [threading.Thread(target=deposit_safe) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"With lock: {balance}")Key points
- Race condition: a bug where timing changes the result
- Use a Barrier to line threads up at the same point
- Use a Lock to let only one thread change shared data at a time
- Use with lock to release the lock when the block ends
- RLock: a lock the same thread can take again
- Deadlock: each thread waits forever for the other, and neither can move
- Synchronization tools protect shared data when threads run at the same time.
In this example, two threads deposit 100 into one shared account. The slow function reads balance into seen first. The barrier then holds both threads until both have read. Both threads read 0, so both write 100. The first print shows the lost deposit as 100. The main code then resets balance to 0. The safe function holds the lock for its read and write. The second thread waits outside the lock until the first finishes. It then reads 100 and writes 200. The second print shows the correct total.
Note
A plain Lock taken twice by one thread will deadlock that thread, so use RLock for nested work
Try it Yourself
Edit the code, then press Run.
Running is simulated · Python Playground
Exercise
Two deposits of 100 gave a balance of 100, not 200. Why?