🎓 A1Learner
🔍

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?