Skip to content

Thread Synchronization with Lock

A race condition happens when two threads update shared state at the same time.

Example: incrementing a shared counter.

race_condition.py
import threading
 
counter = 0
 
 
def inc():
    global counter
    for _ in range(100_000):
        counter += 1
 
 
t1 = threading.Thread(target=inc)
t2 = threading.Thread(target=inc)
 
t1.start(); t2.start()
t1.join(); t2.join()
 
print("counter:", counter)

You might expect 200000, but results can vary.

lock_fix.py
import threading
 
counter = 0
lock = threading.Lock()
 
 
def inc():
    global counter
    for _ in range(100_000):
        with lock:
            counter += 1
 
 
t1 = threading.Thread(target=inc)
t2 = threading.Thread(target=inc)
 
t1.start(); t2.start()
t1.join(); t2.join()
 
print("counter:", counter)
  • Keep locked sections small.
  • Prefer with lock: so you don’t forget to release.
  • Avoid deadlocks (don’t acquire multiple locks in random order).
diagram a lock turns a read-modify-write back into one step mermaid
The unsafe version reads, is interrupted, and writes back a value computed from a stale read -- so one increment disappears. Holding a lock across the whole sequence means no other thread can interleave with it, which is what makes the operation atomic with respect to the others.
sketch Where the increment goes missing, and what the lock removes p5.js
Two threads running read-add-write on one counter. Without a lock the second thread reads before the first has written, so both compute the same result and one increment vanishes. With the lock the second thread waits at the door instead. The counter at the bottom is what each version actually produces.
pch.quizTag pch.quizDefaultTitle
  1. Why can `counter += 1` lose updates across threads?

    pch.quizShowAnswer

    B — It is a read-modify-write, and another thread can run between the read and the write — Both threads read the same old value, compute the same result, and write it. The second write does not add anything — it repeats the first.

  2. An exception is raised inside `with lock:`. What happens to the lock?

    pch.quizShowAnswer

    B — It is released — that is what the context manager guarantees — Verified. A manual `acquire()` with no `try`/`finally` left the lock HELD after an exception, and the next thread to ask for it would wait forever.

  3. The same thread calls `acquire()` twice on a plain `threading.Lock`. What happens?

    pch.quizShowAnswer

    B — It deadlocks against itself — Measured: the second `acquire(timeout=0.2)` returned False. `RLock` records its owner and a count, so the owner may re-enter — and must release the same number of times.

  4. When do you NOT need a lock?

    pch.quizShowAnswer

    B — When the shared state is only ever read — Reads alone cannot tear a value. The moment anything writes, a read-modify-write needs protection — and 'only one writer' is not enough if readers can observe a half-updated structure.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading