Sometimes, threads need to pause and wait for something important to happen before moving on. Condition variables act like traffic lights for threads—they tell them when to stop and when it’s safe to go. In this article, we’ll learn how to use condition variables to make threads wait for a signal and then continue safely, keeping everything running smoothly and in order.
What is a Condition Variable?
A condition variable is a special tool that helps threads talk to each other by waiting for certain conditions to be true. When a thread needs to pause and wait, it can “wait” on the condition variable. Other threads can send a signal (notify) to let waiting threads know it’s time to continue. This way, threads can work together smoothly, coordinating their actions without stepping on each other’s toes.
Using Condition Variables: Basic How-To
First, create a condition variable by calling threading.Condition()
. This object will help threads wait and notify safely.
To use it, place your code inside a with condition:
block. This ensures only one thread accesses the shared resource at a time.
When a thread needs to pause and wait for a signal, it calls condition.wait()
. This releases the lock and waits until another thread sends a notification.
Other threads can wake one waiting thread by calling condition.notify()
, or wake all waiting threads using condition.notify_all()
.
Using condition variables like this helps threads communicate smoothly and avoid chaos.
Simple Example: Waiting for Data
Before we dive into the code, let’s see how condition variables work in action. Imagine one thread waiting for some data to be ready, while another thread prepares the data and then signals that it’s done. This example shows how to make the waiting thread pause safely until the data is ready.
import threading
import time
condition = threading.Condition()
data_ready = False
def waiter():
with condition:
print("Waiting for data...")
condition.wait()
print("Data is ready! Resuming in 1 second...")
time.sleep(1)
def setter():
global data_ready
time.sleep(1) # Simulate preparing data
with condition:
data_ready = True
print("Data prepared, notifying waiter...")
condition.notify()
time.sleep(1) # Pause to show setter still holds lock briefly
t1 = threading.Thread(target=waiter)
t2 = threading.Thread(target=setter)
t1.start()
t2.start()
t1.join()
t2.join()
In this example, the waiter thread pauses with wait()
until the setter thread signals that the data is ready. Both threads have 1-second sleep()
calls to make it easy to see the timing: the waiter waits, then resumes after the notification, and the setter simulates preparing data before notifying. This helps show how threads coordinate safely using condition variables.
Using notify_all()
to Wake Multiple Threads
Sometimes, more than one thread needs to wait for the same signal. Instead of waking one at a time, we can use condition.notify_all()
to signal everyone at once. Think of it like ringing a bell in a classroom — all students wake up at the same time!
Here’s a fun example with three waiter threads. They all wait patiently until the setter thread signals the group:
import threading
import time
condition = threading.Condition()
ready = False
def waiter(id):
with condition:
print(f"Waiter {id} waiting...")
condition.wait()
print(f"Waiter {id} received the go!")
def setter():
global ready
time.sleep(2) # Simulate setup time
with condition:
ready = True
print("Setter: Notifying all waiters!")
condition.notify_all()
# Create and start waiter threads
threads = [threading.Thread(target=waiter, args=(i,)) for i in range(3)]
for t in threads:
t.start()
# Create and start the setter thread
setter_thread = threading.Thread(target=setter)
setter_thread.start()
# Wait for all threads to finish
for t in threads:
t.join()
setter_thread.join()
Each waiter thread pauses using condition.wait()
. When the setter calls condition.notify_all()
, all the waiters are woken up and continue their work. This is a simple and safe way to coordinate multiple threads that need the same green light.
Condition Variables with a Shared State
In real programs, threads usually wait for a certain state to be true — not just a signal. That’s where condition variables shine. A thread might pause and check the condition again when it’s woken up. This is called a spurious wakeup, and it’s why we use a while
loop when waiting.
Here’s a helpful example: a consumer thread waits until there’s an item available. The producer thread adds the item and notifies the consumer.
import threading
condition = threading.Condition()
items = []
def consumer():
with condition:
while not items:
print("Consumer waiting for item...")
condition.wait()
item = items.pop()
print(f"Consumer got item: {item}")
def producer():
with condition:
items.append("cookie")
print("Producer added an item")
condition.notify()
c = threading.Thread(target=consumer)
p = threading.Thread(target=producer)
c.start()
p.start()
c.join()
p.join()
The consumer checks while not items:
before calling wait()
. This way, if it wakes up for any reason other than a real signal, it checks again. This pattern keeps your threads safe, clear, and correct.
Best Practices
When working with condition variables, a few habits make your code safer and easier to understand:
- Always use a
with
block when callingwait()
,notify()
, ornotify_all()
. This ensures that the lock tied to the condition is properly acquired and released. - Wrap
wait()
in awhile
loop that checks the actual condition you’re waiting for. Threads can wake up unexpectedly, so it’s good to re-check. - Use
notify()
when only one thread needs to proceed. Usenotify_all()
when you want to wake up all waiting threads. - Keep the locked block short. Holding a lock too long can block other threads unnecessarily. Do only what’s needed while the lock is held, then let others in.
These habits keep your threaded code smooth, predictable, and easy to debug.
Summary Table
Here’s a quick reference for using condition variables:
Action | Method | Description |
---|---|---|
Wait for condition | condition.wait() | Pause the thread until it gets a signal. |
Wake one thread | condition.notify() | Wake up one waiting thread. |
Wake all threads | condition.notify_all() | Wake up all waiting threads. |
This table helps keep the key methods in mind when coordinating threads with conditions.
Conclusion
Condition variables are like polite messengers for threads — they help threads wait their turn and notify each other when it’s time to go. They’re especially useful when threads share data or need to coordinate actions in order.
By using wait()
, notify()
, and notify_all()
properly inside a with
block and checking conditions with a loop, you can keep your threads organized and cooperative.
Try adding condition variables into your own multi-threaded programs — whether it’s a chat system, a kitchen queue, or a train station simulation. It’s a fun way to make your threads talk.