Last Updated: February 2, 2026
Imagine two threads that need to coordinate a simple handoff. One thread prepares some data. The other must not proceed until that preparation is complete. The intent is straightforward: one thread finishes its work and tells the other, “you can go now.”
This is the essence of signaling. It is not a conversation or a handshake. It is a one-way notification from a signaler to a waiter. The signaler does its job, sends the signal, and immediately moves on. The waiter blocks until that signal arrives, then continues execution. One side is active, the other is reactive.
The challenge is that naïve signaling is fragile. If the signal arrives before the waiter starts waiting, the notification can be lost, leaving the waiter blocked forever.
This chapter introduces the Signaling Pattern, which makes one-way coordination reliable by using primitives that remember signals, rather than treating them as fleeting events.
At its core, signaling is a one-way notification. Thread A does some work, then tells Thread B: "I'm done, you can proceed." Thread B, which has been waiting for exactly this message, wakes up and continues with whatever it needs to do next.
The key insight here is asymmetry. Both threads don't wait for each other. There's a clear sender and a clear receiver. The sender says "go" and immediately moves on to other things. The receiver blocks until it hears "go," then springs into action. One side is active, the other is reactive.
So how do you actually implement this? With a semaphore initialized to zero.
Think about what that means. A semaphore tracks permits. When you call acquire(), you're asking for a permit. If none are available, you block. When you call release(), you're adding a permit. If anyone is waiting, they wake up and grab it.
A semaphore at zero means the gate is closed. No permits available. The waiter arrives, calls acquire(), finds nothing to take, and blocks. Meanwhile, the signaler is off doing its work. When it finishes, it calls release(), adding a single permit. The semaphore wakes up the waiting thread, hands over the permit, and the waiter proceeds.
That's the entire mechanism. Initialize to zero. Waiter acquires. Signaler releases. Done.
| Property | Description |
|---|---|
| One-way | Signaler notifies waiter, not vice versa |
| Non-blocking for signaler | release() never blocks |
| Blocking for waiter | acquire() blocks until signal |
| Decoupled | Signaler doesn't wait for waiter to receive |
| Persistent | If signal arrives before waiter, waiter won't block |
That last property, persistence, deserves a moment. It's what makes semaphores so reliable for signaling compared to condition variables. With a condition variable, if you signal and nobody is listening, that signal evaporates into the void. Lost forever.
But semaphores remember. When Thread A calls release(), the permit count increments from 0 to 1. That "1" sits there, waiting. If Thread B shows up five milliseconds later and calls acquire(), it finds that permit immediately available. No blocking needed. The signal was stored in the semaphore's internal count, patiently waiting for whoever would eventually need it.
This persistence is why semaphores are often the safer choice for signaling. You don't have to carefully orchestrate timing to ensure the waiter is already waiting before the signal arrives.
Every synchronization pattern you encounter is just signaling in disguise.
Walking through a concrete example will make the mechanics clear.
Here's a common situation. You have worker threads that need to access a shared resource, maybe a database connection pool, a configuration object, or a cache. But that resource isn't ready yet. The main thread is still initializing it. If workers start accessing it before initialization completes, chaos ensues. Null pointers, partial state, data corruption.
So the workers need to wait. And the main thread needs to tell them when it's safe to proceed.
Initial State:
Step-by-Step Execution:
Notice something important here. At t=0ms, the worker thread called acquire() and blocked. It didn't spin in a loop checking a flag. It didn't waste CPU cycles. The operating system suspended it entirely, and that thread consumed no resources while waiting. Only when the main thread called release() did the OS wake the worker and let it proceed.
This is the efficiency of proper signaling. The waiter sleeps. The signaler wakes it. No polling, no busy-waiting, no wasted cycles.
This might seem like a small detail, but it's actually the heart of the pattern.
Think about what the initial value means. A semaphore at 1 says: "One permit is available right now." A semaphore at 0 says: "No permits available. If you want one, you'll have to wait for someone to create it."
For signaling, we want the second behavior. The gate starts closed. Anyone who arrives before the signal should block. Only when the signaler explicitly calls release() does a permit appear, and only then can the waiter proceed.
If you accidentally initialize to 1, your "waiter" calls acquire(), immediately gets the permit, and merrily proceeds, oblivious to the fact that initialization hasn't finished yet. The entire point of coordination is defeated. This is one of the most common signaling bugs, and it's worth committing to memory: for signaling, always initialize to zero.
With the concepts solid, let's see signaling in action. The basic pattern is universal, but each language has its own idioms and standard library support. We'll start with the simplest case and build toward more sophisticated patterns.
The fundamental example: one thread prepares data, another thread waits for it. The signaler creates the data and releases a permit. The waiter blocks until that permit appears, then uses the data safely.
Now that we understand the basic mechanism, let's look at how signaling combines into larger patterns. They're patterns you'll commonly encounter in interviews and real-world concurrent code.
Here's a scenario that comes up in almost every concurrent application. You have multiple worker threads that need to access some shared resource. But that resource requires initialization, loading config files, establishing database connections, warming caches. Workers can't start until initialization completes.
The solution is an initialization gate: a semaphore that blocks all workers until the main thread signals that everything is ready.
Notice the key insight in the implementation: the main thread releases N permits, one for each worker. Each worker acquires exactly one permit. This is a one-to-many signal, one signaler wakes up many waiters.
Here's where signaling gets clever. Two threads need to take turns. Thread A does something, signals Thread B, then waits. Thread B does something, signals Thread A, then waits. Back and forth, like a ping-pong ball.
The trick is using two semaphores. One controls when Thread A can go. The other controls when Thread B can go. Initially, we give Thread A permission (semaphore at 1) and make Thread B wait (semaphore at 0). After A finishes, it signals B. After B finishes, it signals A. The two semaphores pass control back and forth like a baton in a relay race.
If both semaphores started at 1, both threads would race to print first. If both started at 0, both would deadlock waiting for the other to signal. The asymmetric initialization (one at 1, one at 0) is what creates the orderly alternation.
You've got options when it comes to thread coordination. Semaphores, mutexes, condition variables, each has its place. Knowing when to reach for signaling versus something else is the mark of a seasoned concurrent programmer.
At first glance, semaphores and mutexes look similar. Both involve acquiring and releasing. Both can block threads. But their purposes are fundamentally different.
A mutex protects data. One thread locks it, does its work, and unlocks it. The same thread does both operations. Nobody else can touch the protected data while the mutex is held. It's about exclusive access.
A signaling semaphore coordinates threads. One thread releases it, a completely different thread acquires it. It's not about protecting data. It's about saying "I'm done, you can proceed."
| Aspect | Semaphore Signaling | Mutex |
|---|---|---|
| Purpose | One thread notifies another | Protect shared data |
| Ownership | No ownership (any thread can signal) | Owned by locking thread |
| Initial state | 0 (waiting) or 1+ (permits available) | Unlocked |
| Thread relationship | Different thread releases than acquires | Same thread locks and unlocks |
| Use case | "Data ready" notification | "Only I can access this data" |
The thread relationship row is the critical distinction. If the same thread that acquired needs to release, use a mutex. If a different thread will signal completion, use a semaphore.
This comparison trips up even experienced developers. Both can be used for signaling. Both involve waiting and waking. But they have different guarantees and failure modes.
The big difference is persistence. A semaphore remembers signals. Call release() on a semaphore with no waiters, and the permit count increases. When a waiter eventually arrives, it finds the permit waiting. The signal wasn't lost.
Condition variables forget. Call signal() on a condition variable with no waiters, and nothing happens. The signal evaporates. When a waiter eventually arrives, it blocks, potentially forever if no one signals again. This is why condition variables require careful protocol design, typically with a predicate and a while loop.
| Aspect | Semaphore | Condition Variable |
|---|---|---|
| Signal persistence | Persists (stored in count) | Lost if no one waiting |
| Spurious wakeups | Not possible | Must use while loop |
| Associated lock | None required | Requires associated mutex |
| Typical use | One-shot signals, counting | Complex wait conditions |
When should you use which? Semaphores are simpler for basic "event happened" signals where you don't need to check additional conditions. Condition variables are more flexible when you need to wait for arbitrary predicates like "the queue has space" or "the count is below threshold."
The diagram makes the difference stark. With semaphores, the signal survives even if no one is listening. With condition variables, the signal requires a listener to matter.