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:

  1. 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.
  2. notify(n=1): Wakes up up to n threads currently suspended in the wait() 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.
  3. 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:

  1. Acquisition: Thread A acquires the lock using with condition: to inspect the state.
  2. 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.
  3. Modification: Thread B acquires the now-free lock using with condition:, modifies the shared state (adds an item to the queue), and calls condition.notify().
  4. Handoff: Thread B exits its context manager, releasing the lock.
  5. Resumption: Thread A re-acquires the lock, exits the condition.wait() call, re-evaluates the while condition, confirms the state is now valid, and completes its operation safely.