Python Central HubSign in

Python Programming / Python Threading

Thread Synchronization with Lock

Learn race conditions and fix them with threading.Lock to safely share data between threads.

The problem: race conditions

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

Example: incrementing a shared counter.

Python
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.

Fix with Lock

Python
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)

Best practices

  • 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.

Why the counter goes wrong

The line counter += 1 looks like a single step, but Python runs it as three: read the current value, add one, write the result back. Now imagine two threads. Both read 100, both add one, and both write 101. Two increments happened but the counter only moved once. The interleaving depends on exactly when the operating system switches threads, so the error is random. On some Python versions and machines the result looks correct for small loops, which makes the bug even more dangerous: it can hide in testing and appear in production.

How the fix works

A Lock is like a key to a single room. A thread must hold the key to enter the block, and while it does, any other thread that asks for the key waits outside. In lock_fix.py the read-add-write sequence is inside with lock:, so the two threads take turns, one increment at a time, and the answer is always 200000. The price is speed: the protected part runs one thread at a time, so keep it as small as possible.

What should be locked?

Lock the code that touches the shared data, and nothing else. The heavy work should stay outside:

Python
import threading
 
total = 0
lock = threading.Lock()
 
 
def worker(numbers: list[int]) -> None:
    global total
    partial = sum(n * n for n in numbers)   # slow part: no lock needed
    with lock:                              # quick part: update shared state
        total += partial
 
 
chunks = [list(range(0, 1000)), list(range(1000, 2000)), list(range(2000, 3000))]
threads = [threading.Thread(target=worker, args=(c,)) for c in chunks]
for t in threads:
    t.start()
for t in threads:
    t.join()
 
print(total)  # 8995500500

Each thread computes its own partial sum privately and takes the lock only for the single addition to total. This is a common pattern: work on local variables, then merge under a lock.

Common mistakes

  • Each thread creating its own lock. A lock only works if all threads share the same one.
  • Locking only the writes. Reads of data that is being modified need the lock too.
  • Using acquire() and forgetting release() after an exception. The with statement is safer.
  • Holding the lock during slow work such as network calls, which turns your threads into a queue.
  • Nesting locks in different orders, which leads to deadlock.

When to use it

Use a lock whenever several threads change the same variable, list, dictionary or file. If you find yourself adding many locks, consider passing data through a queue.Queue instead, so threads never share mutable state at all.

Check yourself

quizCheck yourself
  1. Why can `counter += 1` lose updates across threads?

    Show answer

    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?

    Show answer

    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?

    Show answer

    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?

    Show answer

    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.

🧪 Try It Yourself

Exercise 1 – Basic Lock Usage

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Lock as Context Manager

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – Detect Locked State

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did