Synchronize State with Python threading.Condition
In Python's threading module,
threading.Condition allows multiple threads to coordinate
by waiting for specific state changes before proceeding. Rather than
having threads constantly poll a shared resource—wasting CPU cycles—a
condition variable enables threads to suspend execution until another
thread modifies the shared state and signals them. This article explains
the internal mechanics of threading.Condition, breaks down
the wait-and-notify workflow, and demonstrates the standard design
pattern required to synchronize application state reliably.
The Underlying Lock Architecture
Every threading.Condition object is intrinsically tied
to an underlying lock, which is an RLock (reentrant lock)
by default, though a standard Lock can be passed explicitly
during initialization. The condition variable cannot alter or inspect
shared state without first acquiring this lock.
The lock ensures mutual exclusion when checking or modifying the shared state, while the condition object manages the queue of threads sleeping while waiting for a particular state predicate to become true.
The Core Methods:
wait(), notify(), and
notify_all()
Thread synchronization through a condition variable relies on three primary methods, all of which require the calling thread to currently hold the associated lock:
wait(timeout=None): Releases the underlying lock and blocks the thread, placing it into an internal wait queue. When another thread later notifies this sleeping thread,wait()wakes up and automatically re-acquires the lock before returning.notify(n=1): Wakes up up tonthreads currently suspended in thewait()queue. The notified threads do not immediately resume execution; they move from the waiting queue to a state where they contend to re-acquire the lock once the notifying thread releases it.notify_all(): Wakes up all threads currently waiting on the condition variable. This is typically used when a state change could potentially satisfy the conditions for multiple waiting threads simultaneously.
The Standard Synchronization Pattern
Because threads release the lock when calling wait() and
must re-acquire it upon waking, the shared state may change between the
moment a thread is notified and the moment it successfully re-acquires
the lock. Another thread might intervene and invalidate the
condition.
Consequently, waiting threads must always evaluate the predicate
inside a while loop, never an if
statement.
import threading
queue = []
MAX_ITEMS = 5
condition = threading.Condition()
def consumer():
with condition:
# Always check state in a while loop
while len(queue) == 0:
condition.wait()
item = queue.pop(0)
print(f"Consumed: {item}")
condition.notify_all()
def producer(item):
with condition:
while len(queue) >= MAX_ITEMS:
condition.wait()
queue.append(item)
print(f"Produced: {item}")
condition.notify_all()State Synchronization Flow
The typical execution flow between two threads synchronizes state as follows:
- Acquisition: Thread A acquires the lock using
with condition:to inspect the state. - Suspension: The state is not ready (for example,
the queue is empty). Thread A calls
condition.wait(). This atomically releases the lock and suspends Thread A. - Modification: Thread B acquires the now-free lock
using
with condition:, modifies the shared state (adds an item to the queue), and callscondition.notify(). - Handoff: Thread B exits its context manager, releasing the lock.
- Resumption: Thread A re-acquires the lock, exits
the
condition.wait()call, re-evaluates thewhilecondition, confirms the state is now valid, and completes its operation safely.