Free lesson · Concurrency Interview Mastery
The Signaling Pattern
The previous modules gave you primitives (locks, condition variables, atomics) and a catalog of what goes wrong with them. This module assembles those primitives into patterns - reusable structures that solve recurring concurrency problems, the way design patterns solve recurring object-oriented ones. We start with the most fundamental: signaling, the pattern by which one thread tells another that something has happened. Almost every higher pattern in this module - producer-consumer, futures, thread pools, barriers - is built on signaling underneath, so understanding it well is the foundation for everything that follows. Signaling is how threads coordinate events rather than just protect data: locks answer "is it safe to touch this?", while signaling answers "has this happened yet, and wake me when it does." Getting signaling right means getting the lost-and-spurious-wakeup discipline from Module 6 right, which makes this both a pattern lesson and a consolidation of correct condition-variable use.
What the signaling pattern is
The signaling pattern coordinates two roles: a waiter that needs some condition to become true before it can proceed, and a signaler that makes the condition true and notifies the waiter. Instead of the waiter repeatedly checking ("are we there yet? are we there yet?"), it sleeps until the signaler wakes it. This is the efficient alternative to busy-waiting: the waiter consumes no CPU while blocked, and the signaler wakes it precisely when there's a reason to.
The canonical implementation uses a condition variable with the lock-and-loop discipline from Module 6:
// WAITER: // SIGNALER:
lock() lock()
while not condition: condition = true // make it true
cond.wait() // sleep, release lock cond.signal() // wake a waiter
// condition is now true -> proceed unlock()
unlock()
The waiter acquires the lock, checks the condition in a while loop, and if it's not yet true, calls wait() - which atomically releases the lock and sleeps. The signaler acquires the same lock, sets the condition true, and signals. The lock serializes the two, the while loop guards against lost and spurious wakeups, and the result is correct, efficient event coordination. This is the pattern in its purest form, and it's worth recognizing that you've already built its correctness foundation - the signaling pattern is "use a condition variable correctly," elevated to a named, reusable structure.
sequenceDiagram
participant W as Waiter
participant L as Shared Condition Lock
participant S as Signaler
W->>L: Acquire lock and check condition
Note over W: Sleeps and releases lock\nNo CPU used
S->>L: Acquire lock
S->>L: Set condition to true
S->>L: Signal waiting thread
L-->>W: Wake waiter
W->>L: Recheck condition
W->>L: Condition is true
W->>L: Proceed and unlock
Note over W: Efficient wakeup when condition becomes true
This sequence diagram shows the signaling pattern. The waiter acquires the lock and checks the condition in a while loop; since it is not yet true, it calls wait, which puts the waiter to sleep and releases the lock, so the waiter uses no CPU while blocked. Later, the signaler acquires the same lock, sets the condition to true, and calls signal. This wakes the waiter. The waiter re-checks the condition, finds it now true, proceeds with its work, and releases the lock. The key benefit is efficiency: the waiter slept until there was an actual reason to wake, rather than busy-looping and wasting CPU checking the condition over and over.
Why not just busy-wait?
The alternative to signaling is busy-waiting (or spinning): the waiter loops, repeatedly checking a flag, until it becomes true.
// BUSY-WAIT (usually wasteful):
while not ready:
// spin, burning CPU, checking again and again
pass
proceed()
Busy-waiting has a real cost: the spinning thread consumes a full CPU core doing nothing useful, checking a condition that may not change for milliseconds or seconds. With many waiters or long waits, this wastes enormous CPU and can starve other threads of cores. Signaling eliminates the waste - the waiter sleeps (yielding its core) and is woken only when the condition actually changes. The general rule: use signaling for anything but the very shortest waits. (Spinning is occasionally justified for extremely brief waits where the cost of sleeping and waking exceeds the spin - the spinlock case from earlier modules - but for ordinary event coordination, signaling is the correct, efficient choice.) The whole point of the signaling pattern is to replace wasteful polling with efficient sleep-and-wake.
Variants
The signaling pattern comes in several forms depending on how many waiters to wake and how often signaling happens:
- signal (notify one) vs broadcast (notifyAll).
signal()wakes one waiting thread;notifyAll()/broadcast wakes all of them. Usesignal()when any single waiter can handle the event and all waiters are equivalent; usenotifyAll()when waiters wait for different conditions (so the right one can proceed and the rest re-wait) or when multiple waiters should all proceed. As Module 6 noted,notifyAll()is the safer default when in doubt, because thewhileloop makes waking extra threads harmless. - One-shot signaling (latches and events). Some signals fire once and stay fired: a latch or event that, once set, remains set, so any thread that waits on it afterward proceeds immediately. This is for one-time events like "initialization complete" or "the system is ready" - the signal has memory, unlike a bare condition variable. A countdown latch is a common form: wait until N events have occurred, then release all waiters permanently.
- Repeated signaling. A condition variable signals each time the condition becomes true and is consumed - the producer-consumer queue signals on every item added. Here the signal has no memory; it coordinates an ongoing stream of events rather than a one-time transition.
Important: The signaling pattern replaces wasteful busy-waiting with efficient sleep-and-wake: a waiter blocks until a signaler notifies it that a condition is met. Implement it with the lock +
while-loop discipline (Module 6) - the lock serializes check-and-wait against set-and-signal (preventing lost wakeups), and thewhileloop re-checks on every wake (handling spurious and stolen wakeups). Choosesignal()when waiters are equivalent and any one suffices,notifyAll()when waiters await different conditions or all should proceed (the safer default). Distinguish one-shot signals with memory (latches/events - fire once, stay fired, for "ready"/"done" events) from repeated signals without memory (condition variables - for ongoing streams). Signaling is the primitive beneath producer-consumer, futures, and thread pools, so getting it right underpins every later pattern.
Where signaling appears
Signaling is everywhere in concurrent systems, usually inside higher abstractions:
- Producer-consumer (next-but-one lesson): the producer signals "item available," the consumer signals "space available."
- Futures/promises (later lesson): completing a future signals all threads awaiting its result.
- Thread pools (next lesson): submitting a task signals an idle worker; the queue's not-empty condition is a signal.
- Latches and barriers: a latch signals all waiters when a count reaches zero; a barrier signals when all parties arrive.
- Initialization gates: worker threads wait on a "system ready" signal before processing.
Recognizing signaling as the shared substrate of these patterns is the conceptual payoff: they differ in what event is signaled and how many wait, but they all reduce to "wait efficiently for a condition; wake when it's met," which is exactly the pattern this lesson establishes.
Beyond condition variables: other signaling mechanisms
Condition variables are the most general signaling tool, but several other primitives are signaling mechanisms in specialized forms, and recognizing them as such unifies the landscape:
- Semaphores as signals. A semaphore (Module 3) can be used purely for signaling: a thread waits by acquiring (decrementing), and another signals by releasing (incrementing). A binary semaphore used this way is a one-bit signal; a counting semaphore can signal "N units of something are available." Semaphores have a subtle advantage over condition variables for signaling: a semaphore remembers signals - a release before any acquire isn't lost, it increments the count, so a later acquire succeeds immediately. This sidesteps the lost-wakeup problem that bare condition variables have, which is one reason semaphores are sometimes preferred for simple signaling.
- Events and latches. An event (or
Eventobject) is an explicit one-bit signal withset(),clear(), andwait()operations - a purpose-built signaling primitive. A countdown latch signals when a counter reaches zero. These are signaling with a clean, intention-revealing API. - Futures and promises. Completing a future (a later lesson) signals every thread awaiting its result - a future is a one-shot signal carrying a value. Awaiting a future is waiting on a signal.
- Channels. In channel-based concurrency (Go, Module 2), sending on a channel signals a receiver; the channel is the signaling mechanism, carrying both notification and data.
The unifying view: all of these are ways to make a thread wait efficiently for an event and be woken when it occurs. They differ in API, in whether the signal carries data, and in whether the signal has memory (semaphores and one-shot latches remember; bare condition variables don't), but they're all the signaling pattern in different clothing. Choosing among them is mostly about fit - a semaphore for counted resources, an event for a one-time flag, a future for a result, a condition variable for an arbitrary predicate - but they share the same conceptual core.
A worked example: completion signaling
A common signaling use is completion notification - one thread does work, and others wait until it's done. Here's the pattern with a condition variable:
// Shared:
lock = Lock(); cond = Condition(lock); done = false; result = null
// Worker that does the job and signals completion:
function doJob():
r = computeExpensiveResult()
lock()
result = r
done = true // set the condition under the lock
cond.notifyAll() // wake ALL waiters - they all want the result
unlock()
// Any number of threads waiting for the result:
function awaitResult():
lock()
while not done: # while-loop predicate
cond.wait()
r = result # done is true -> result is ready
unlock()
return r
The worker computes, then under the lock sets result, flips done, and notifyAll()s - waking every thread blocked in awaitResult(). Each waiter's while not done loop guards against spurious wakeups and ensures it only proceeds once the result is genuinely ready. Crucially, this uses notifyAll() because all waiters want the same completion event, and it sets done before signaling under the lock so no completion notification can be lost. Note that this completion-signal pattern is precisely what a future/promise packages into a reusable abstraction (a later lesson): "compute a value, signal everyone waiting on it." Seeing that a future is just completion-signaling with a clean API is the kind of connection that reveals how the patterns in this module build on one another - signaling is the mechanism, the future is the convenient wrapper. Recognizing the raw signaling beneath a high-level abstraction like a future is exactly the understanding that lets you reason about, and debug, concurrent code built on those abstractions.
Note: The reason signaling deserves its own lesson at the head of a patterns module - rather than being folded into the condition-variable lesson - is that it's the conceptual unit the other patterns are composed from, and naming it explicitly changes how you read concurrent code. Once you see "wait efficiently for an event, wake when it happens" as a single named pattern, you start recognizing it everywhere: the blocking queue a thread pool worker waits on is signaling; the future a thread awaits is signaling; the barrier that releases when all parties arrive is signaling; the channel receive that blocks until a send is signaling. Each higher pattern is signaling specialized to a particular event and a particular number of waiters. This is the same move that makes design patterns valuable in object-oriented code - giving a recurring structure a name so you can see it across superficially different situations. When you can look at a producer-consumer queue, a future, and a countdown latch and see "these are all signaling," you understand the family rather than memorizing each member, and you can reason about and debug any of them by reasoning about the shared mechanism underneath.
Tip: When you reach for signaling, decide three things up front, and the rest follows: what event you're signaling, how many threads wait for it, and whether the signal needs memory. The event determines the predicate the waiters loop on. The count determines
signal()(one equivalent waiter) versusnotifyAll()(many, or heterogeneous waiters - the safer default). And the memory question determines the mechanism: a bare condition variable for a transient, repeated event (a stream where each notification is consumed); a one-shot latch, event, or semaphore for a transition that must persist so late waiters still see it (a one-time "ready"/"done"). Most signaling bugs trace to getting one of these three wrong - a transient signal used where a persistent one was needed (lost wakeup), orsignal()used wherenotifyAll()was needed (a notification reaching the wrong waiter). Answer the three questions deliberately and the correct primitive and discipline fall out.
The interview angle
Interview: Common questions: (1) "What's the signaling pattern?" - One thread waits for a condition; another signals when it's met, so the waiter sleeps efficiently instead of busy-waiting. (2) "How do you implement it correctly?" - Lock +
while-loop predicate: waiter holds the lock, checks the condition in a loop, callswait()(which releases the lock and sleeps); signaler sets the condition and signals under the same lock. The loop handles spurious/lost/stolen wakeups. (3) "signal vs notifyAll?" -signal()wakes one (use when waiters are equivalent);notifyAll()wakes all (use when waiters await different conditions; safer default). (4) "Why not busy-wait?" - Spinning burns a CPU core checking a condition that rarely changes; signaling sleeps and wakes only on the event. (5) "Where is it used?" - Underneath producer-consumer, futures, thread pools, latches, barriers. Connecting signaling to the lock-and-loop discipline and naming it as the substrate of higher patterns are the depth signals.
Quick reference
The atom of thread coordination - wait efficiently for an event, wake when it happens - and the foundation every other pattern in this module is built on. The implementation discipline is the lock-plus-while-loop from Module 6, and the main design choices are how many waiters to wake and whether the signal remembers it fired:
SIGNALING PATTERN: a WAITER sleeps until a SIGNALER notifies it a condition is met
-> efficient sleep-and-wake, NOT busy-waiting (which burns a CPU core spinning)
CORRECT IMPLEMENTATION (lock + while-loop discipline, from Module 6):
WAITER: lock(); while(!condition) cond.wait(); ...proceed...; unlock()
SIGNALER: lock(); condition = true; cond.signal(); unlock()
lock serializes check-and-wait vs set-and-signal (no lost wakeup);
while loop re-checks every wake (handles spurious/stolen wakeups)
VARIANTS:
signal (notify ONE) -> waiters equivalent, any one suffices
notifyAll (broadcast) -> waiters await different conditions / all proceed (safer default)
ONE-SHOT (latch/event): fires once, STAYS fired (has memory) -> "ready"/"done" events
REPEATED (condition var): fires each time, no memory -> ongoing streams (producer-consumer)
WHY NOT BUSY-WAIT: spinning wastes a full CPU core; sleep-and-wake is efficient
(spin only for the very shortest waits - the spinlock exception)
SUBSTRATE OF: producer-consumer, futures/promises, thread pools, latches, barriers
Signaling is the atom of thread coordination. The next pattern is the workhorse of practical concurrency, built directly on signaling and the producer-consumer structure: the thread pool, which solves the thread-leakage problem from Module 6 by reusing a bounded set of worker threads to execute a stream of tasks.