Loading content...
If the wait operation is how threads go to sleep waiting for conditions, the signal operation is how they're awakened. But signaling is not as simple as it might appear. When you signal, you face several crucial decisions:
These questions reveal the subtleties that make concurrent programming challenging. A wrong choice can lead to bugs ranging from performance degradation to system deadlock.
In this page, we'll thoroughly explore the signal operation, its sibling broadcast, and the decision-making framework for choosing between them. We'll examine the semantics, implementations, and the common patterns that make signals effective.
By the end of this page, you will understand: the precise semantics of signal and broadcast operations; when to use signal versus broadcast; the implications of signaling with or without the mutex held; how signals are implemented at the kernel level; and common signaling patterns and anti-patterns.
POSIX provides two ways to wake waiting threads:
int pthread_cond_signal(pthread_cond_t *cond); // Wake ONE waiter
int pthread_cond_broadcast(pthread_cond_t *cond); // Wake ALL waiters
The difference is simple to state but profound in implication:
Signal: Wakes at most one thread from the condition variable's wait queue. If no threads are waiting, the signal has no effect (it is not remembered).
Broadcast: Wakes all threads currently waiting on the condition variable. Again, if no threads are waiting, nothing happens.
Why two operations?
The existence of both operations reflects a fundamental tradeoff between correctness and performance.
| Aspect | Signal (wake one) | Broadcast (wake all) |
|---|---|---|
| Threads woken | At most 1 | All waiters |
| Performance | Higher (less context switching) | Lower (many wakeups, most blocked again) |
| Risk | May not wake the "right" thread | Thundering herd problem |
| Correctness | Requires careful analysis | Always correct (but may be slow) |
| Use case | Multiple waiters for same condition | Waiters for different conditions |
The correctness question:
Signal is an optimization. Broadcast is always correct (assuming correct wait loop patterns), but signal may be incorrect if used improperly. Consider:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Scenario: Two types of waiters on the SAME condition variable// This is a BAD design, but illustrates the problem pthread_cond_t cond;pthread_mutex_t mutex;int items = 0;int special_items = 0; // Waiter A: Waits for any itemvoid* waiter_any(void* arg) { pthread_mutex_lock(&mutex); while (items == 0) { pthread_cond_wait(&cond, &mutex); } items--; pthread_mutex_unlock(&mutex); return NULL;} // Waiter B: Waits for special items onlyvoid* waiter_special(void* arg) { pthread_mutex_lock(&mutex); while (special_items == 0) { // Different condition! pthread_cond_wait(&cond, &mutex); } special_items--; pthread_mutex_unlock(&mutex); return NULL;} // Producer adds a special itemvoid add_special_item(void) { pthread_mutex_lock(&mutex); special_items++; items++; pthread_cond_signal(&cond); // BUG: Might wake waiter_any! pthread_mutex_unlock(&mutex);} // The problem:// - waiter_any and waiter_special BOTH wait on 'cond'// - add_special_item calls signal()// - Signal might wake waiter_any instead of waiter_special// - waiter_any finds items > 0, consumes a regular item// - waiter_special stays asleep forever!Signal is only safe when ALL waiters on a condition variable wait for the SAME predicate. If different waiters wait for different conditions, signal may wake the wrong thread. In such cases, use separate condition variables or broadcast.
Let's precisely define what signal does:
pthread_cond_signal:
Key properties:
12345678910111213141516171819202122232425262728
// Demonstrating signal behavior in different scenarios // Scenario 1: One waiter// ----------------------// Thread A: wait(&cond, &mutex) -- blocked// Thread B: signal(&cond) -- wakes A// Thread A: reacquires mutex, returns from wait // Scenario 2: Multiple waiters// ---------------------------// Thread A: wait(&cond, &mutex) -- blocked// Thread B: wait(&cond, &mutex) -- blocked// Thread C: signal(&cond) -- wakes ONE (A or B)// Only one thread proceeds; the other remains blocked // Scenario 3: No waiters// ----------------------// Thread A: signal(&cond) -- nothing happens// Thread B: wait(&cond, &mutex) -- blocks (signal was lost)// Thread B will wait forever unless signaled again // Scenario 4: Signal before predicate change (BAD)// -----------------------------------------------// Thread A: waiting for (count > 0)// Thread B: signal(&cond) -- A wakes// count++; -- too late!// Thread A: checks (count > 0), it's FALSE, waits again// This works due to while loop, but wastes a context switchMesa vs. Hoare semantics revisited:
The signal semantics we've described are Mesa semantics (signal-and-continue):
Contrast with Hoare semantics (signal-and-wait):
No modern system uses Hoare semantics in its pure form—the immediate context switch is too expensive. Mesa semantics dominate, which is why the while loop is mandatory.
Broadcast wakes all waiting threads:
pthread_cond_broadcast:
The thundering herd problem:
Broadcast has a notorious performance issue. When N threads are woken:
12345678910111213141516171819202122232425262728293031
// The thundering herd problem illustrated // Setup: 100 threads waiting for workfor (int i = 0; i < 100; i++) { pthread_mutex_lock(&mutex); while (work_queue_empty()) { pthread_cond_wait(&work_available, &mutex); } item = dequeue_work(); pthread_mutex_unlock(&mutex); process(item);} // Producer adds ONE item and broadcastsvoid add_work(Item* item) { pthread_mutex_lock(&mutex); enqueue_work(item); pthread_cond_broadcast(&work_available); // Wakes ALL 100! pthread_mutex_unlock(&mutex);} // What happens:// 1. All 100 threads wake up// 2. They all try to acquire the mutex// 3. Thread 1 wins, dequeues the item, releases mutex// 4. Thread 2 acquires mutex, finds queue empty, waits again// 5. Thread 3 acquires mutex, finds queue empty, waits again// ... repeated 98 more times!// // Result: 1 item processed, 99 pointless context switches// This is VERY expensive on systems with many threadsIn the example above, adding one work item causes 100 threads to wake, 99 to immediately block again, with potentially hundreds of context switches. With thousands of threads, systems can become unresponsive. This is why signal (wake one) exists.
When broadcast is necessary:
Despite the thundering herd problem, broadcast is sometimes required:
1234567891011121314151617181920212223242526272829303132333435
// Examples where broadcast is correct and necessary // Example 1: Shutdown notificationvoid shutdown_server(void) { pthread_mutex_lock(&mutex); shutting_down = true; pthread_cond_broadcast(&cond); // Wake ALL threads to exit pthread_mutex_unlock(&mutex);} // Example 2: Resource pool with multiple availablevoid return_connections(Connection* conns, int n) { pthread_mutex_lock(&mutex); for (int i = 0; i < n; i++) { add_to_pool(conns[i]); available_count++; } pthread_cond_broadcast(&pool_cond); // Wake up to n waiters pthread_mutex_unlock(&mutex);} // Example 3: Barrier synchronizationvoid barrier_wait(Barrier* b) { pthread_mutex_lock(&b->mutex); b->count++; if (b->count == b->threshold) { // Last thread: wake everyone pthread_cond_broadcast(&b->cond); // Must be broadcast! } else { while (b->count < b->threshold) { pthread_cond_wait(&b->cond, &b->mutex); } } pthread_mutex_unlock(&b->mutex);}Choosing between signal and broadcast requires careful analysis. Here's a decision framework:
Rule 1: When in doubt, use broadcast.
Broadcast is always correct (given proper while loops). It may be slower due to spurious wakeups, but it won't cause missed wakeups or deadlocks.
Rule 2: Use signal only when ALL of the following are true:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Case 1: Work queue with identical workers// Condition: work queue not empty// All workers wait for same predicate - USE SIGNAL void add_task(Task* t) { pthread_mutex_lock(&mutex); enqueue(t); pthread_cond_signal(&cond); // Signal is safe and efficient pthread_mutex_unlock(&mutex);} // Case 2: Readers-writers lock// Readers wait for: no writers// Writers wait for: no readers AND no writers// Different predicates - USE BROADCAST (or separate CVs) void writer_unlock(RWLock* lock) { pthread_mutex_lock(&lock->mutex); lock->writer = false; // Don't know if we should wake reader or writer pthread_cond_broadcast(&lock->cond); // Must broadcast pthread_mutex_unlock(&lock->mutex);} // Case 3: Bounded buffer producer// Condition: not full// Adding one slot enables one producer// All producers want same thing - USE SIGNAL void consume(void) { pthread_mutex_lock(&mutex); // ... consume item ... count--; pthread_cond_signal(¬_full); // Signal is safe pthread_mutex_unlock(&mutex);} // Case 4: Configuration change// All threads need to see new configvoid update_config(Config* new_config) { pthread_mutex_lock(&mutex); current_config = new_config; pthread_cond_broadcast(&config_cond); // Wake ALL pthread_mutex_unlock(&mutex);}| Scenario | Choice | Rationale |
|---|---|---|
| Single consumer, single producer | Signal | Only one waiter possible |
| Thread pool work queue | Signal | Uniform workers, one task per signal |
| Barrier synchronization | Broadcast | All threads must proceed together |
| Shutdown notification | Broadcast | All threads need to know |
| Readers-writers lock | Broadcast* | *Or use separate CVs |
| Resource pool returning N items | Broadcast | Up to N threads can proceed |
| Condition state change tracking | Broadcast | All observers need update |
A perennial question: should you hold the mutex when calling signal or broadcast?
POSIX allows both:
The specification permits calling signal/broadcast without holding the mutex. However, the implications are subtle.
Option A: Signal while holding mutex (recommended)
12345678910111213141516171819202122
// Pattern A: Signal while holding mutexvoid producer(Item item) { pthread_mutex_lock(&mutex); buffer[in] = item; in = (in + 1) % SIZE; count++; pthread_cond_signal(¬_empty); // Signal while locked pthread_mutex_unlock(&mutex);} // Pros:// - Simpler reasoning: modification and signal are atomic// - No race window between modify and signal// - Waiter wakes and blocks on mutex (predictable) // Cons:// - Woken thread immediately blocks on mutex// - Extra context switch in some cases// - Minor: holds mutex slightly longerOption B: Signal after releasing mutex
1234567891011121314151617181920212223
// Pattern B: Signal after releasing mutexvoid producer(Item item) { pthread_mutex_lock(&mutex); buffer[in] = item; in = (in + 1) % SIZE; count++; pthread_mutex_unlock(&mutex); pthread_cond_signal(¬_empty); // Signal after unlock} // Pros:// - Woken thread may acquire mutex immediately// - Potentially fewer context switches// - Mutex held for shorter time // Cons:// - Race window: another thread could run between// unlock and signal, changing state again// - More complex reasoning about program behavior// - Potential for subtle bugs in complex scenariosFor most applications, signaling while holding the mutex is simpler and safer. The performance difference is usually negligible. Only unlock-then-signal if profiling shows it matters AND you've carefully analyzed the race conditions.
The "hurry up and wait" problem:
When you signal while holding the mutex:
This creates an extra context switch that unlock-then-signal avoids. However, modern kernels often optimize this with "wait morphing"—the woken thread is moved directly to the mutex's wait queue without fully waking it.
When unlock-then-signal can go wrong:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Subtle bug with unlock-then-signal in complex scenario // Thread A: Consumer with timeoutvoid consumer_with_timeout(void) { pthread_mutex_lock(&mutex); while (count == 0) { int status = pthread_cond_timedwait(&cond, &mutex, &deadline); if (status == ETIMEDOUT) { pthread_mutex_unlock(&mutex); return; // Timed out } } // Consume item Item item = buffer[out]; out = (out + 1) % SIZE; count--; pthread_mutex_unlock(&mutex); process(item);} // Thread B: Producer with unlock-then-signalvoid producer_unlock_first(Item item) { pthread_mutex_lock(&mutex); buffer[in] = item; in = (in + 1) % SIZE; count++; pthread_mutex_unlock(&mutex); // Unlock first // <<< RACE WINDOW >>> // Thread C could: // 1. Lock mutex // 2. Consume the item we just added // 3. Unlock mutex // Now count is 0 again! pthread_cond_signal(&cond); // Signal (for Thread A) // Thread A wakes, but count is 0! // A's while loop rechecks, goes back to wait // Not a bug (while loop saves us), but a wasted wakeup}Understanding how signal is implemented helps in reasoning about its behavior:
Linux (NPTL) Implementation:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// Simplified Linux signal implementation (conceptual) int pthread_cond_signal(pthread_cond_t *cond) { // 1. Lock condition variable's internal spinlock spin_lock(&cond->__lock); // 2. Check if anyone is waiting if (cond->__nwaiters == 0) { spin_unlock(&cond->__lock); return 0; // No waiters, nothing to do } // 3. Increment wakeup sequence number cond->__wakeup_seq++; cond->__futex++; // This change will wake futex waiters // 4. Unlock and wake one waiter spin_unlock(&cond->__lock); // FUTEX_WAKE wakes one thread waiting on this address futex(&cond->__futex, FUTEX_WAKE, 1); return 0;} int pthread_cond_broadcast(pthread_cond_t *cond) { spin_lock(&cond->__lock); if (cond->__nwaiters == 0) { spin_unlock(&cond->__lock); return 0; } // Wake ALL by signaling nwaiters times cond->__wakeup_seq += cond->__nwaiters; cond->__futex++; spin_unlock(&cond->__lock); // FUTEX_WAKE with INT_MAX wakes all waiters futex(&cond->__futex, FUTEX_WAKE, INT_MAX); return 0;}The futex_wake operation:
The kernel maintains a hash table of futex wait queues. When FUTEX_WAKE is called:
Wait morphing optimization:
Linux implements an optimization for the "hurry up and wait" problem. When a thread is woken from a condition variable and will immediately block on a mutex:
This avoids an unnecessary wake-block cycle and reduces context switches.
Windows, macOS, and other systems have different implementations. The key invariants (signal-one, broadcast-all) are maintained, but performance characteristics may differ. Always benchmark on your target platform if performance is critical.
Let's examine patterns that work well and patterns to avoid.
Pattern: Single-slot handoff
12345678910111213141516171819202122232425262728293031
// Pattern: Single producer, single consumer, single slot// Uses signal correctly pthread_mutex_t mutex;pthread_cond_t has_data;pthread_cond_t has_space;int slot_full = 0;Data slot; void producer(Data d) { pthread_mutex_lock(&mutex); while (slot_full) { pthread_cond_wait(&has_space, &mutex); } slot = d; slot_full = 1; pthread_cond_signal(&has_data); // Exactly one consumer waiting pthread_mutex_unlock(&mutex);} Data consumer(void) { pthread_mutex_lock(&mutex); while (!slot_full) { pthread_cond_wait(&has_data, &mutex); } Data d = slot; slot_full = 0; pthread_cond_signal(&has_space); // Exactly one producer waiting pthread_mutex_unlock(&mutex); return d;}Anti-pattern: Forgetting to signal
123456789101112
// ANTI-PATTERN: Forgetting to signal void buggy_producer(Data d) { pthread_mutex_lock(&mutex); enqueue(d); count++; // BUG: Forgot to signal! pthread_mutex_unlock(&mutex);} // Consumers will wait forever if they were already// waiting when this producer ranAnti-pattern: Signal without state change
1234567891011121314
// ANTI-PATTERN: Signaling without changing state void confusing_code(void) { pthread_mutex_lock(&mutex); // No state change here! pthread_cond_signal(&cond); // Pointless signal pthread_mutex_unlock(&mutex);} // This causes spurious wakeups with no benefit// Waiters wake, check condition, and wait againThe signal operation appears across many languages with similar semantics:
Java:
123456789101112131415161718192021222324
// Java: notify() and notifyAll()// Associated with Object, not separate condition variable synchronized (lock) { // Modify state ready = true; lock.notify(); // Wake one waiter (signal) // or lock.notifyAll(); // Wake all waiters (broadcast)} // Java's Lock interface with explicit ConditionsLock lock = new ReentrantLock();Condition notEmpty = lock.newCondition();Condition notFull = lock.newCondition(); lock.lock();try { items++; notEmpty.signal(); // Wake one on this specific condition} finally { lock.unlock();}C++11:
123456789101112131415161718192021
// C++11: std::condition_variable #include <condition_variable>#include <mutex> std::mutex mtx;std::condition_variable cv; // Signal one waiter{ std::lock_guard<std::mutex> lock(mtx); ready = true;}cv.notify_one(); // Can be called without lock // Signal all waiters{ std::lock_guard<std::mutex> lock(mtx); finished = true;}cv.notify_all();Python:
123456789101112131415161718
# Python: threading.Condition import threading cv = threading.Condition() # With context manager (recommended)with cv: ready = True cv.notify() # Wake one # or cv.notify_all() # Wake all # notify(n) wakes up to n threadswith cv: for i in range(5): add_resource() cv.notify(5) # Wake up to 5 waiters| Language | Signal One | Signal All | Notes |
|---|---|---|---|
| C (pthreads) | pthread_cond_signal() | pthread_cond_broadcast() | Explicit mutex parameter in wait |
| Java | notify() | notifyAll() | Must hold intrinsic lock (synchronized) |
| C++11 | notify_one() | notify_all() | Can call without lock held |
| Python | notify() | notify_all() | Use with 'with' statement |
| Rust | notify_one() | notify_all() | Returns number notified |
The signal operation is how threads communicate that conditions may have changed. Let's consolidate the key points:
Decision flowchart:
Are all waiters waiting for the same predicate?
├── Yes: Is making the predicate true sufficient for
│ exactly one waiter to proceed?
│ ├── Yes: Use signal
│ └── No: Use broadcast
└── No: Can you split into separate condition variables?
├── Yes: Split them, then use signal on each
└── No: Use broadcast
What's next:
Now that we understand both wait and signal, we'll explore the relationship between condition variables and mutexes. We'll see why they're inseparable, how to properly pair them, and what happens when this relationship is violated.
You now understand the signal operation in depth: when to use signal versus broadcast, how to decide, and the implementation details that affect behavior. This knowledge is essential for writing efficient concurrent code that doesn't suffer from deadlocks or wasted resources.