Thread Synchronization with Lock
The problem: race conditions
Section titled “The problem: race conditions”A race condition happens when two threads update shared state at the same time.
Example: incrementing a shared counter.
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
Section titled “Fix with Lock”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
Section titled “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).
flowchart TD A["counter += 1"] --> B["read counter"] B --> C["add 1"] C --> D["write counter"] B -.->|"another thread can run HERE"| E["it reads the SAME old value"] E --> F["both write the same result"] F --> G["one increment is lost"] H["with lock:"] --> I["read, add, write -- uninterrupted by other threads"] I --> J["the invariant holds"]
Check yourself
Section titled “Check yourself”-
Why can `counter += 1` lose updates across threads?
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.
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.
-
An exception is raised inside `with lock:`. What happens to the lock?
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.
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.
-
The same thread calls `acquire()` twice on a plain `threading.Lock`. What happens?
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.
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.
-
When do you NOT need a lock?
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.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.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Basic Lock Usage
Section titled “Exercise 1 – Basic Lock Usage”Exercise 2 – Lock as Context Manager
Section titled “Exercise 2 – Lock as Context Manager”Exercise 3 – Detect Locked State
Section titled “Exercise 3 – Detect Locked State”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading