AlgoMaster Logo

Signaling Pattern

Last Updated: February 2, 2026

Ashish

Ashish Pratap Singh

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.

What is Signaling?

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.

The Core Mechanism

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.

Key Properties

PropertyDescription
One-waySignaler notifies waiter, not vice versa
Non-blocking for signalerrelease() never blocks
Blocking for waiteracquire() blocks until signal
DecoupledSignaler doesn't wait for waiter to receive
PersistentIf 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.

Benefits of Sigaling

Every synchronization pattern you encounter is just signaling in disguise.

  • Barrier: That's N-1 threads signaling the Nth thread, and the Nth thread signaling the other N-1 back. Everyone waits, then everyone signals, then everyone proceeds.
  • Latch: One thread signals many waiters that an event occurred, and the waiters wake up to continue.
  • Producer-consumer: The producer signals the consumer: "data's ready."
  • Thread pools: Workers signal the manager: "task complete, give me another."

How It Works

Walking through a concrete example will make the mechanics clear.

Scenario: Initialization Gate

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:

  • Semaphore initialized to 0 (the gate is closed)
  • Worker thread has started but can't do useful work yet
  • Main thread is busy preparing the shared resource

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.

Flowchart: Signaling Decision Logic

Why Semaphore(0)?

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.

Implementation

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.

Basic Signal Pattern

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.

Common Patterns Using Signaling

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.

Pattern 1: Initialization Gate

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.

Pattern 2: Ping-Pong (Alternating Execution)

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.

Signaling vs Other Primitives

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.

Semaphore Signaling vs Mutex

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."

AspectSemaphore SignalingMutex
PurposeOne thread notifies anotherProtect shared data
OwnershipNo ownership (any thread can signal)Owned by locking thread
Initial state0 (waiting) or 1+ (permits available)Unlocked
Thread relationshipDifferent thread releases than acquiresSame 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.

Semaphore Signaling vs Condition Variable

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.

AspectSemaphoreCondition Variable
Signal persistencePersists (stored in count)Lost if no one waiting
Spurious wakeupsNot possibleMust use while loop
Associated lockNone requiredRequires associated mutex
Typical useOne-shot signals, countingComplex 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.