Threads & Locks
20 min
"Increment a counter" looks atomic in source code (counter += 1) but
is actually three separate steps at the hardware level: read the
current value, compute the new value, write it back. When two
threads run this without coordination, their steps can interleave in
ways that lose updates — this is a race condition.
def simulate_race(schedule, initial_value=0):
shared = initial_value
thread_local = {}
for thread_id, step in schedule:
if step == "read":
thread_local[thread_id] = shared
elif step == "write":
shared = thread_local[thread_id] + 1
return shared
A lock's entire job is to make the 'BAD' interleaving impossible -- it forces every thread's read+write pair to complete as one uninterrupted unit before another thread's read can start, which is exactly the 'GOOD' schedule above.
Write `simulate_race(schedule, initial_value=0)`: `schedule` is a list of `(thread_id, step)` pairs where `step` is `'read'` (thread caches the current shared value) or `'write'` (thread writes cached-value + 1 back to shared). Simulate the schedule in order and return the final shared value — this models an UNSYNCHRONIZED read-modify-write increment.
Two threads both increment a shared counter once, with no lock, starting from 0. What's the WORST-case final value, and why?
You can simulate an explicit thread interleaving to see exactly how an unsynchronized read-modify-write produces a lost update.