A lock protects an invariant, not a variable.
Think about the state that must stay consistent, then make the whole read–check–write operation participate in the same protocol.
Name the rule that must hold
Suppose a worker pool tracks its available slots. The useful rule is that a worker can claim a slot only when one is available. Checking the count and decrementing it are two parts of one logical operation.
If callers use different locks, or bypass the lock, they do not participate in the same synchronization protocol.
Keep the operation together
For threads sharing this in-process state, a single threading.Lock can guard both the check and the update. This snippet illustrates slot accounting; it is not a complete task scheduler.
from threading import Lock
slots = 2
slots_lock = Lock()
def claim_slot():
global slots
with slots_lock:
if slots == 0:
return False
slots -= 1
return True
def release_slot():
global slots
with slots_lock:
slots += 1Match the primitive to the participants
Every successful claim must have exactly one release. Keep longer-running work outside the critical section when possible and use try/finally around that work to ensure release on exceptions.
A thread lock in one process does not coordinate separate processes. Async tasks also need primitives appropriate to their runtime: Python documents that asyncio synchronization primitives are not thread-safe.
Start from who shares the state, what consistency rule must hold, and which operations can interleave. Those answers determine the synchronization boundary.
References: Python thread locks ↗ · Asyncio synchronization primitives ↗