Loading learning content...
While pthread_cond_wait() allows threads to suspend until a condition becomes true, pthread_cond_signal() is the complementary operation that makes a condition variable system complete. It is the mechanism by which a thread announces that a condition may have changed, waking at least one waiting thread to check.
The signal operation is deceptively simple—a single function call with no parameters beyond the condition variable itself. Yet its semantics, timing, and interaction with the mutex and scheduler require careful understanding for correct usage.
This page provides a comprehensive exploration of pthread_cond_signal()—its semantics, guarantees, the signaling-under-lock debate, wake ordering, and its role in the producer-consumer pattern that underlies countless concurrent systems.
By completing this page, you will: (1) Master the pthread_cond_signal() function signature and semantics, (2) Understand the 'at least one' wakeup guarantee, (3) Navigate the signal-under-lock versus signal-after-unlock debate, (4) Know how wake ordering is determined, (5) Apply signal correctly in producer-consumer patterns, and (6) Distinguish when signal vs. broadcast is appropriate.
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
Parameters:
cond: Pointer to a properly initialized condition variable. This is the condition variable on which waiting threads may be blocked.Return Value:
0: Success. If threads were waiting, at least one has been awakened.EINVAL for invalid cond).When a thread calls pthread_cond_signal():
If one or more threads are blocked on cond:
If no threads are waiting on cond:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
#include <pthread.h>#include <stdio.h>#include <stdbool.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;bool data_ready = false;int shared_data = 0; // =====================================================// Producer: Generates Data and Signals Consumer// ===================================================== void producer_work(int value) { pthread_mutex_lock(&mutex); // Produce the data shared_data = value; data_ready = true; printf("Producer: data = %d, signaling consumer", value); // Signal one waiting consumer pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);} // =====================================================// Consumer: Waits for Data and Processes It// ===================================================== void *consumer_thread(void *arg) { pthread_mutex_lock(&mutex); while (!data_ready) { printf("Consumer: waiting for data..."); pthread_cond_wait(&cond, &mutex); } // Process the data printf("Consumer: processing data = %d", shared_data); data_ready = false; // Mark as consumed pthread_mutex_unlock(&mutex); return NULL;} // =====================================================// Signal with No Waiters: A No-Op// ===================================================== void signal_with_no_waiters(void) { // This is perfectly valid, though does nothing pthread_cond_signal(&cond); // The signal simply "evaporates" // Future waiters will NOT be affected by this signal printf("Signaled with no waiters - no effect");}A common misconception is that signaling 'passes' the mutex to the woken thread. It does not. The signaling thread retains the mutex until it explicitly unlocks. The woken thread blocks on the mutex until the signaler releases it. This is by design—the signaler may need to do additional work after signaling.
One of the most debated questions in condition variable usage is: should you signal while holding the mutex, or after releasing it?
Both approaches are technically correct from a POSIX perspective, but they have different implications for performance, correctness, and code clarity.
pthread_mutex_lock(&mutex);
modify_shared_state();
pthread_cond_signal(&cond); // Signal while holding lock
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
modify_shared_state();
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); // Signal after releasing lock
Signaling under lock can cause what's called the "hurry up and wait" pattern:
This means the woken thread briefly wakes up only to immediately block on the mutex. This seems wasteful, but modern implementations optimize for this case (wait morphing, direct handoff).
Despite the potential for "hurry up and wait", signaling under lock is the safer default:
1. Correctness Simplicity
With signal-under-lock, you guarantee that:
2. Avoiding Lost Wakeups
Consider this subtle bug with signal-after-unlock:
// Thread A (signaler)
pthread_mutex_lock(&mutex);
flag = true;
pthread_mutex_unlock(&mutex)
// --- Context switch here ---
pthread_cond_signal(&cond); // Signal delayed!
// Thread B (waiter) runs here
pthread_mutex_lock(&mutex);
while (!flag) { // flag is true!
// Doesn't enter loop
}
// Thread B proceeds, never waited
// Thread C (another waiter)
pthread_mutex_lock(&mutex);
while (!flag) { // flag is now false (consumed by B)
pthread_cond_wait(&cond, &mutex); // Blocks
}
// Thread A finally signals
pthread_cond_signal(&cond); // Wakes Thread C
In this example, the signal-after-unlock is fine. But what if Thread B had already been waiting when Thread A modified flag? Without signal-under-lock, reasoning becomes complex.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <pthread.h>#include <stdbool.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;bool work_available = false; // =====================================================// RECOMMENDED: Signal Under Lock// ===================================================== void producer_signal_under_lock(void) { pthread_mutex_lock(&mutex); work_available = true; // Signal while holding lock - clear, safe, predictable pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);} // =====================================================// ALTERNATIVE: Signal After Unlock// (Valid but requires careful reasoning)// ===================================================== void producer_signal_after_unlock(void) { pthread_mutex_lock(&mutex); work_available = true; pthread_mutex_unlock(&mutex); // Signal after unlock - woken thread can immediately get mutex // BUT: timing between unlock and signal is observable pthread_cond_signal(&cond);} // =====================================================// The Wait Morphing Optimization// (What modern implementations do)// ===================================================== /* * Modern pthreads implementations (glibc NPTL, etc.) use * "wait morphing" or "direct handoff" to optimize the * signal-under-lock case: * * 1. Signaler signals while holding mutex * 2. Woken thread is moved from cond wait queue to mutex wait queue * (Instead of: wake fully, try to lock, block on mutex) * 3. When signaler releases mutex, woken thread is granted it directly * * Result: No redundant wake-then-block cycle. * The "hurry up and wait" problem is optimized away. * * This optimization makes signal-under-lock have the same * performance as signal-after-unlock in practice. */Signal while holding the mutex. The potential micro-optimization of signal-after-unlock is not worth the cognitive overhead and subtle race risk. Modern implementations' wait morphing eliminates the performance concern. Keep your code simple and correct.
When multiple threads are waiting on a condition variable and pthread_cond_signal() is called, which thread wakes up? POSIX deliberately leaves this implementation-defined, providing flexibility for different scheduling policies.
"If more than one thread is blocked on a condition variable, the scheduling policy shall determine the order in which threads are unblocked."
This means:
Different implementations make different choices based on efficiency, fairness, or priority concerns.
1. FIFO (First-In-First-Out)
Many implementations use FIFO for fairness—the longest-waiting thread wakes first. But this is not guaranteed.
2. LIFO (Last-In-First-Out)
Some implementations use a stack-like structure for efficiency—recently blocked threads have hot caches.
3. Priority-Based
With real-time scheduling policies (SCHED_FIFO, SCHED_RR), higher-priority waiters may be preferred.
4. Arbitrary
Some implementations make no ordering guarantees whatsoever.
| Implementation | Default Ordering | With Real-Time | Notes |
|---|---|---|---|
| glibc (Linux NPTL) | FIFO (mostly) | Priority + FIFO within priority | Uses futex; kernel scheduling affects order |
| musl libc | Simple FIFO | FIFO | Simpler implementation |
| macOS libpthread | Unspecified | Priority-aware | Kernel-scheduled |
| Windows (SRW) | FIFO | N/A | SRWLock condition variables |
| FreeBSD | FIFO within priority | Priority | Turnstile-based |
When Ordering Doesn't Matter:
In most producer-consumer scenarios, all consumer threads are equivalent. Any consumer can process any item. Wake ordering is irrelevant—work gets done regardless of which thread handles it.
When Ordering Matters:
Designing for Order-Independence:
Best practice is to design systems where wake ordering doesn't affect correctness:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
#include <pthread.h>#include <stdbool.h>#include <stdlib.h> // =====================================================// Order-Independent Design: Any Worker, Any Job// ===================================================== typedef struct { pthread_mutex_t mutex; pthread_cond_t work_available; int *work_queue; int queue_head; int queue_tail; int queue_capacity; bool shutdown;} thread_pool_t; // Worker doesn't care about wake order - grabs next job from queuevoid *worker(void *arg) { thread_pool_t *pool = (thread_pool_t *)arg; while (1) { pthread_mutex_lock(&pool->mutex); // Wait for work (any work, not specific work) while (pool->queue_head == pool->queue_tail && !pool->shutdown) { pthread_cond_wait(&pool->work_available, &pool->mutex); } if (pool->shutdown && pool->queue_head == pool->queue_tail) { pthread_mutex_unlock(&pool->mutex); break; } // Grab the next job (whichever thread gets here first) int job = pool->work_queue[pool->queue_head]; pool->queue_head = (pool->queue_head + 1) % pool->queue_capacity; pthread_mutex_unlock(&pool->mutex); // Process job (order of processing may differ from submission) process_job(job); } return NULL;} // =====================================================// Priority-Aware Design: Separate Condition Variables// ===================================================== typedef struct { pthread_mutex_t mutex; pthread_cond_t high_priority_work; pthread_cond_t low_priority_work; // Separate queues...} priority_pool_t; // High-priority work signals high-priority conditionvoid submit_high_priority(priority_pool_t *pool, int job) { pthread_mutex_lock(&pool->mutex); add_to_high_queue(pool, job); pthread_cond_signal(&pool->high_priority_work); pthread_mutex_unlock(&pool->mutex);} // Workers check high-priority first// This ensures high-priority work is handled promptly// regardless of implementation wake orderingIf your correctness depends on specific wake ordering, your design is fragile. Restructure to be order-independent. If you need priority handling, use separate condition variables or priority queues explicit in your code, not implicit in thread wake ordering.
POSIX provides two wakeup operations: signal (wake one) and broadcast (wake all). Choosing correctly is essential for both correctness and performance.
| Operation | Behavior | Performance | Correctness Risk |
|---|---|---|---|
pthread_cond_signal() | Wakes at least one waiter | Best | May miss waiters if misused |
pthread_cond_broadcast() | Wakes all waiters | Scales with waiter count | Always safe |
Use pthread_cond_signal() when:
Only one waiter can make progress — E.g., one item added to queue, only one consumer should process it.
All waiters are equivalent — Any one waiter handling the event is sufficient.
The condition will be rechecked anyway — Your while-loop pattern ensures correctness.
Canonical example: Producer adds one item → signal one consumer.
Use pthread_cond_broadcast() when:
Multiple waiters can/should make progress — E.g., condition changed from "empty" to "non-empty" and multiple items are now available.
Waiter predicates differ — Multiple threads wait on the same cond var but for different conditions.
State changed in a way that releases multiple waiters — E.g., shutdown signal.
You're uncertain — Broadcast is always correct (though may be slower).
Canonical example: Shutdown flag set → broadcast to all workers.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#include <pthread.h>#include <stdbool.h> // =====================================================// Signal: One Item, One Consumer// ===================================================== typedef struct { pthread_mutex_t mutex; pthread_cond_t not_empty; int *items; int count;} simple_queue_t; void queue_push(simple_queue_t *q, int item) { pthread_mutex_lock(&q->mutex); add_item(q, item); // ONE item added → wake ONE consumer pthread_cond_signal(&q->not_empty); pthread_mutex_unlock(&q->mutex);} int queue_pop(simple_queue_t *q) { pthread_mutex_lock(&q->mutex); while (q->count == 0) { pthread_cond_wait(&q->not_empty, &q->mutex); } int item = remove_item(q); pthread_mutex_unlock(&q->mutex); return item;} // =====================================================// Broadcast: Shutdown All Workers// ===================================================== typedef struct { pthread_mutex_t mutex; pthread_cond_t cond; bool shutdown; int active_workers;} worker_pool_t; void pool_shutdown(worker_pool_t *pool) { pthread_mutex_lock(&pool->mutex); pool->shutdown = true; // ALL workers need to know about shutdown pthread_cond_broadcast(&pool->cond); pthread_mutex_unlock(&pool->mutex);} // =====================================================// Broadcast: Multiple Items Added// ===================================================== void queue_push_batch(simple_queue_t *q, int *items, int n) { pthread_mutex_lock(&q->mutex); for (int i = 0; i < n; i++) { add_item(q, items[i]); } // Multiple items → multiple consumers could proceed // Broadcast is correct here pthread_cond_broadcast(&q->not_empty); // Alternative: signal n times (less efficient) // for (int i = 0; i < n; i++) { // pthread_cond_signal(&q->not_empty); // } pthread_mutex_unlock(&q->mutex);} // =====================================================// Broadcast: Different Predicates, Same Cond Var// ===================================================== int shared_count = 0; void *thread_wait_for_even(void *arg) { pthread_mutex_lock(&mutex); while (shared_count % 2 != 0) { // Wait for EVEN pthread_cond_wait(&cond, &mutex); } // Process even state... pthread_mutex_unlock(&mutex); return NULL;} void *thread_wait_for_gt_10(void *arg) { pthread_mutex_lock(&mutex); while (shared_count <= 10) { // Wait for > 10 pthread_cond_wait(&cond, &mutex); } // Process > 10 state... pthread_mutex_unlock(&mutex); return NULL;} void update_count(int new_value) { pthread_mutex_lock(&mutex); shared_count = new_value; // Different threads check different conditions // Must broadcast so both can recheck pthread_cond_broadcast(&cond); pthread_mutex_unlock(&mutex);}Using signal when you should broadcast causes lost wakeups. Example: Two consumers wait for different conditions on the same cond var. Producer signals. Consumer A wakes, rechecks, condition A is false, goes back to sleep. Consumer B (whose condition is true) was never woken. Consumer B waits forever. Always broadcast when waiters have different predicates.
Understanding how pthread_cond_signal() is implemented helps predict performance and debug issues.
When no threads are waiting, signal is extremely fast:
Total: ~5-10 cycles — essentially free.
This enables the pattern of always signaling after state changes without worrying about the cost when no one is waiting.
When threads are waiting:
Total: ~3,000-6,000 cycles — dominated by the system call.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// =====================================================// Conceptual Implementation of pthread_cond_signal// (Simplified for understanding)// ===================================================== struct __pthread_cond_t { unsigned int __total_seq; // Total number of waits started unsigned int __wakeup_seq; // Number of wakeups signaled unsigned int __woken_seq; // Number of threads actually woken pthread_mutex_t __internal_lock; unsigned int __nwaiters; // Current waiter count // ... futex references, etc.}; int pthread_cond_signal(pthread_cond_t *cond) { struct __pthread_cond_t *c = (struct __pthread_cond_t *)cond; // FAST PATH: No waiters // This is an atomic read - no locking needed for check if (__atomic_load_n(&c->__nwaiters, __ATOMIC_RELAXED) == 0) { return 0; // Nothing to do } // SLOW PATH: There are waiters // Need to wake at least one // Acquire internal lock __lock(&c->__internal_lock); if (c->__nwaiters > 0) { // Increment wakeup sequence number // This is how waiters detect they should wake c->__wakeup_seq++; // Use futex to wake one thread // FUTEX_WAKE with count=1 futex_wake(&c->__futex_word, 1); } __unlock(&c->__internal_lock); return 0;} // =====================================================// Wait Morphing Optimization (Modern Implementations)// ===================================================== /* * Instead of: * signal() → futex_wake() → thread runs → pthread_mutex_lock() blocks * * Modern implementations do: * signal() → move thread from cond_waitqueue to mutex_waitqueue * * The thread never fully wakes; it just moves queues. * When mutex is released, thread is already first in line. * * This requires coordination between: * - pthread_cond_signal implementation * - pthread_mutex_unlock implementation * - Kernel futex handling * * Result: No "hurry up and wait" overhead. */| Scenario | Cycles | Notes |
|---|---|---|
| No waiters | 5-10 | Just atomic check, immediate return |
| One waiter, wait morphing | ~500 | Queue manipulation, no context switch |
| One waiter, full wakeup | 3,000-6,000 | System call required |
| Multiple waiters (signal) | 3,000-6,000 | Same as one waiter |
| Multiple waiters (broadcast) | N × 3,000-6,000 | Must wake all N |
The cost of signal is driven by whether waiters exist and how many. Design systems where threads rarely need to wait: larger buffers, work batching, or lock-free algorithms. A signal with no waiters costs essentially nothing.
The producer-consumer pattern is the canonical use case for condition variables and pthread_cond_signal(). Let's examine a complete, production-quality implementation.
This requires two condition variables:
not_full: Signaled when buffer becomes non-full (producers wait on this)not_empty: Signaled when buffer becomes non-empty (consumers wait on this)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
#include <pthread.h>#include <stdlib.h>#include <stdio.h>#include <stdbool.h> // =====================================================// Production-Quality Bounded Buffer// ===================================================== typedef struct { pthread_mutex_t mutex; pthread_cond_t not_empty; // Signaled when: count goes 0 → 1 pthread_cond_t not_full; // Signaled when: count goes capacity → capacity-1 int *buffer; int capacity; int count; // Current number of items int head; // Next position to read from int tail; // Next position to write to bool shutdown; // For graceful termination} bounded_buffer_t; // =====================================================// Initialization and Destruction// ===================================================== bounded_buffer_t *buffer_create(int capacity) { bounded_buffer_t *bb = malloc(sizeof(bounded_buffer_t)); if (!bb) return NULL; bb->buffer = malloc(sizeof(int) * capacity); if (!bb->buffer) { free(bb); return NULL; } pthread_mutex_init(&bb->mutex, NULL); pthread_cond_init(&bb->not_empty, NULL); pthread_cond_init(&bb->not_full, NULL); bb->capacity = capacity; bb->count = 0; bb->head = 0; bb->tail = 0; bb->shutdown = false; return bb;} void buffer_destroy(bounded_buffer_t *bb) { pthread_cond_destroy(&bb->not_full); pthread_cond_destroy(&bb->not_empty); pthread_mutex_destroy(&bb->mutex); free(bb->buffer); free(bb);} // =====================================================// Producer: Put Item// ===================================================== bool buffer_put(bounded_buffer_t *bb, int item) { pthread_mutex_lock(&bb->mutex); // Wait while full (and not shutdown) while (bb->count == bb->capacity && !bb->shutdown) { pthread_cond_wait(&bb->not_full, &bb->mutex); } if (bb->shutdown) { pthread_mutex_unlock(&bb->mutex); return false; // Cannot put during shutdown } // Add item bb->buffer[bb->tail] = item; bb->tail = (bb->tail + 1) % bb->capacity; bb->count++; // Signal ONE consumer (we added one item) // If buffer was empty, a consumer might be waiting pthread_cond_signal(&bb->not_empty); pthread_mutex_unlock(&bb->mutex); return true;} // =====================================================// Consumer: Get Item// ===================================================== bool buffer_get(bounded_buffer_t *bb, int *item) { pthread_mutex_lock(&bb->mutex); // Wait while empty (and not shutdown) while (bb->count == 0 && !bb->shutdown) { pthread_cond_wait(&bb->not_empty, &bb->mutex); } // Check if woken due to shutdown with empty buffer if (bb->count == 0) { pthread_mutex_unlock(&bb->mutex); return false; // No item available } // Remove item *item = bb->buffer[bb->head]; bb->head = (bb->head + 1) % bb->capacity; bb->count--; // Signal ONE producer (we removed one item) // If buffer was full, a producer might be waiting pthread_cond_signal(&bb->not_full); pthread_mutex_unlock(&bb->mutex); return true;} // =====================================================// Graceful Shutdown// ===================================================== void buffer_shutdown(bounded_buffer_t *bb) { pthread_mutex_lock(&bb->mutex); bb->shutdown = true; // Wake ALL waiters - both producers and consumers // They will see shutdown flag and exit pthread_cond_broadcast(&bb->not_empty); pthread_cond_broadcast(&bb->not_full); pthread_mutex_unlock(&bb->mutex);}Even with pthread_cond_signal()'s simple interface, several common mistakes lead to bugs.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
#include <pthread.h>#include <stdbool.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int work_count = 0; // =====================================================// MISTAKE 1: Forgetting to Signal// ===================================================== void producer_BROKEN_no_signal(void) { pthread_mutex_lock(&mutex); work_count++; // OOPS: Forgot to signal! pthread_mutex_unlock(&mutex); // Consumer waits forever...} void producer_CORRECT(void) { pthread_mutex_lock(&mutex); work_count++; pthread_cond_signal(&cond); // Don't forget! pthread_mutex_unlock(&mutex);} // =====================================================// MISTAKE 2: Signaling Without State Change// ===================================================== void update_BROKEN_empty_signal(void) { pthread_mutex_lock(&mutex); // Do some work, but work_count unchanged pthread_cond_signal(&cond); // Signal wakes consumer... pthread_mutex_unlock(&mutex); // Consumer rechecks condition, still false, goes back to sleep // Wasted wakeup, poor performance} void update_CORRECT(void) { pthread_mutex_lock(&mutex); // Only signal if condition actually changed if (some_work_added) { work_count++; pthread_cond_signal(&cond); } pthread_mutex_unlock(&mutex);} // =====================================================// MISTAKE 3: Using Signal with Different Predicates// ===================================================== bool high_priority_ready = false;bool low_priority_ready = false; void *high_priority_worker(void *arg) { pthread_mutex_lock(&mutex); while (!high_priority_ready) { pthread_cond_wait(&cond, &mutex); } // ... process high priority pthread_mutex_unlock(&mutex); return NULL;} void *low_priority_worker(void *arg) { pthread_mutex_lock(&mutex); while (!low_priority_ready) { pthread_cond_wait(&cond, &mutex); } // ... process low priority pthread_mutex_unlock(&mutex); return NULL;} void set_low_priority_ready_BROKEN(void) { pthread_mutex_lock(&mutex); low_priority_ready = true; pthread_cond_signal(&cond); // WRONG: Might wake high_priority_worker! pthread_mutex_unlock(&mutex); // high_priority_worker wakes, sees high_priority_ready==false, sleeps again // low_priority_worker never woke up!} void set_low_priority_ready_CORRECT(void) { pthread_mutex_lock(&mutex); low_priority_ready = true; pthread_cond_broadcast(&cond); // CORRECT: Both check their predicates pthread_mutex_unlock(&mutex);} // =====================================================// MISTAKE 4: Signaling a Destroyed Condition Variable// ===================================================== void thread_cleanup_BROKEN(pthread_cond_t *cond) { pthread_cond_destroy(cond); // Later, another thread (race condition)... pthread_cond_signal(cond); // UNDEFINED BEHAVIOR!}We have comprehensively explored pthread_cond_signal()—the operation that wakes waiting threads to check changed conditions. Combined with pthread_cond_wait(), it forms the foundation of efficient thread coordination.
You now understand pthread_cond_signal() at production depth. The next page covers pthread_cond_broadcast()—the operation that wakes ALL waiting threads, essential for shutdown patterns and multi-waiter scenarios.