runtime
188 / 250
lesson 188 Threading & Multiprocessing · advanced

Shared state: lost updates and locks

When two threads read and update the same value, a switch between the read and the write loses an update. The bug is rare, silent and timing-dependent; a lock makes the read-modify-write one step.

Read and diagnose: this can't run in the browser, so you'll read real code and output and answer questions about it.

You'll be able to
  • Trace how two interleaved read-modify-write sequences lose an update
  • Protect shared updates with threading.Lock and with lock:
  • Prefer designs that share less: each thread its own result, or a Queue to hand work over
The idea

balance += 10 is a read, an addition and a write (“The interpreter: bytecode and the GIL”). If thread A reads 100, then the scheduler switches to thread B, which also reads 100 and writes 110, A later writes its own 110 and B's deposit is lost. Real threads switch at unpredictable moments, so this happens rarely and differently every run, which makes it a nasty bug.

The example makes the switching visible with a model: each "thread" is a generator that pauses (yield) at the point where a real thread could be switched out, and a schedule decides who runs next. The model is deterministic, so every interleaving can be tried on purpose.

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Two deposits, two schedules
def deposit(account, amount):
    balance = account["balance"]
    yield "read"
    account["balance"] = balance + amount
    yield "wrote"

def run(schedule, workers):
    for name in schedule:
        next(workers[name], None)

account = {"balance": 100}
run(["A", "A", "B", "B"], {"A": deposit(account, 10), "B": deposit(account, 10)})
print(account["balance"])
account = {"balance": 100}
run(["A", "B", "A", "B"], {"A": deposit(account, 10), "B": deposit(account, 10)})
print(account["balance"])

A lock allows only one thread at a time into a section of code. threading.Lock() creates one; with lock: acquires it (waiting if another thread holds it) and releases it at the end of the block, even after an error. Put the whole read-modify-write inside the same with lock: block. Below, the model uses a real threading.Lock: a worker that can't acquire it (acquire(blocking=False) returns False) pauses and tries again, as a real thread would wait.

example-2.py · The same deposits with a lock
import threading

def safe_deposit(account, amount, lock):
    while not lock.acquire(blocking=False):
        yield "waiting"
    try:
        balance = account["balance"]
        yield "read"
        account["balance"] = balance + amount
        yield "wrote"
    finally:
        lock.release()

def run_until_done(workers):
    running = list(workers)
    while running:
        for worker in list(running):
            if next(worker, "done") == "done":
                running.remove(worker)

lock = threading.Lock()
account = {"balance": 100}
run_until_done([safe_deposit(account, 10, lock), safe_deposit(account, 10, lock)])
print(account["balance"], lock.locked())

Locks have costs: code inside them runs one thread at a time, and two locks taken in different orders can deadlock (each thread waits for the other forever). Often the better fix is to share less: give each thread its own result slot and combine after join, or pass work between threads through a queue.Queue, which does its own locking (put and get are safe from any thread).

Mistakes to avoid: locking the write but not the read; using a different lock in different places for the same data; doing slow work (network calls) while holding a lock; and believing the GIL protects your read-modify-write (it doesn't, “The interpreter: bytecode and the GIL”).

Review
  • A read-modify-write split by a thread switch loses updates silently.
  • with lock: around the whole check-and-update makes it one step; create the lock once and share it.
  • Share less where you can: separate results per thread, or a queue.Queue to hand work over.
  • Next: processes, which run Python on several cores at once.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Shared state: lost updates and locks — Threading & Multiprocessing · Runtime