Spurious Wakeups — Why a Condition Variable Wakes "Without a Notification" and How to Wait Correctly on Windows
· Updated: · Go Komura · Windows, Multithreading, Condition Variables, Synchronization, C++, C#, Win32 API, Bug Investigation
Revision history (1 updates, last updated Sep 8, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the current Japanese original. The previous English version was an abridgement that dropped subsections, tables, diagrams, and paragraphs; all of them have been restored to match the Japanese article, and the knowledge map section has been added where the Japanese article has one. The technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.22170893)
- First published
Cite this article(DOI: 10.5281/zenodo.22170892)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Spurious Wakeups — Why a Condition Variable Wakes "Without a Notification" and How to Wait Correctly on Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22170892 https://comcomponent.com/en/blog/windows-condition-variable-spurious-wakeup/
- DOI (latest version)
- 10.5281/zenodo.22170892
- DOI (this version)
- 10.5281/zenodo.22652530
“We put the data in before notifying, yet the worker reads an empty queue and crashes.” “We are sure we notified, yet now and then the thread never returns from its wait.” Both are rendezvous bugs, but the places to look are different.
The center of this article is one principle: returning from a condition variable’s wait does not guarantee that the condition you were waiting for holds. Once you understand spurious wakeups, which return without a notification, and stolen wakeups, where the condition is consumed after the notification, it becomes clear why while is needed rather than if. Lost wakeups, where a notification is missed, are treated separately from these two.
| What you want to know or are stuck on | Where to read |
|---|---|
| Why a wait wakes without a notification, and how that differs from a stolen wakeup | What is guaranteed at the moment of return, Why the specification allows it |
| Correct code for Win32, C++, and C# | The basic shape per language |
| A timeout was specified, yet the wait keeps getting longer | Deadline-based waiting |
| A thread does not return even though it was notified | Lost wakeups and PulseEvent |
| Investigating a crash or hang that only happens rarely | Investigation steps by symptom |
The intended readers are developers who write business applications and equipment-control software on Windows. The article confirms the mechanism from primary sources and connects it to Win32 (C), C++, and C# implementations.
1. The Conclusion First
Wait code has to keep three rules.
| Principle | What the code must do |
|---|---|
| Wait for state, not for a notification | Make the condition “is the queue non-empty?” and the like, not “was I woken?” |
| Re-check the condition every time you return | while (!condition) wait(...); in C++, use the predicate overload wait(lock, pred) |
| Protect the check and the update with the same lock | Update the state before notifying, so that no notification slips through the gap between the check and the wait |
Win32, C++, and POSIX all allow, by specification, wakeups that are not tied to an explicit notification. On top of that, even when a notification arrives, another thread can consume the condition first (a stolen wakeup), so the re-check is mandatory. C#’s Monitor.Wait is used under the same discipline, with stolen wakeups in mind.12345
The other caution is not to recreate a condition variable’s transient notification with a pulse on an event. PulseEvent in particular can miss a notification, and Microsoft’s guidance is not to use it in new applications and to use a condition variable instead.6
The rest of the article proceeds in this order: how wakeups work (Sections 2 to 4), the correct implementation (Section 5), patterns to avoid and how to investigate (Sections 6 and 7), and a checklist (Section 8).
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (17 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. What a Spurious Wakeup Is — “Woken” Does Not Mean “Condition Holds”
2.1 A Condition Variable Releases the Lock, Waits, and Reacquires It Before Returning
A condition variable is a synchronization primitive that makes a thread wait until some condition holds. On Win32 it uses the following structure and APIs.1
| Role | Win32 type or API |
|---|---|
| Represents the condition variable | CONDITION_VARIABLE |
| Releases the lock and waits | SleepConditionVariableCS / SleepConditionVariableSRW |
| Wakes waiting threads | WakeConditionVariable / WakeAllConditionVariable |
The wait API atomically releases the critical section or SRW lock the thread holds and enters the wait. After waking, it reacquires that lock before returning to the caller.1
Reacquiring the lock and returning, however, is a different thing from the condition the application was waiting for holding.
2.2 Distinguishing a Genuine Notification, a Spurious Wakeup, and a Stolen Wakeup
Leaving the handling of timeouts to Section 5, first compare the three wakeup cases.
| Case | Relation to an explicit notification | How to read the return |
|---|---|---|
| Genuine wakeup | A notification exists | The condition often holds, but the fact of returning alone does not guarantee it |
| Spurious wakeup | Not tied to an explicit notification meant to wake this thread | Returns even though the condition still does not hold |
| Stolen wakeup | A notification exists | Another thread consumed the condition first, so it no longer holds |
flowchart TB
accTitle: Three cases in which wait returns
accDescr: A condition variable wait returns not only from a genuine notification but also from a spurious wakeup with no notification and from a stolen wakeup where a notification arrived but the condition was consumed first, so the condition must be re-checked in every case
w["Returned from wait"] --> a["Genuine notification"]
w --> b["Spurious wakeup (no notification)"]
w --> c["Stolen wakeup (condition already consumed)"]
a --> r["Re-check the condition, then proceed"]
b --> r
c --> r
Figure 1: There are three paths back from wait, and the caller cannot tell which one it took, so the condition must always be re-checked.
Spurious wakeups are not limited to the case where no notification has been sent anywhere in the system. When notifications arrive in a short burst, the implementation may, for its own reasons, wake extra waiting threads in a batch. From the point of view of a thread that has no corresponding explicit notification, that too is a spurious wakeup.
Microsoft Learn states that condition variables are subject to both spurious wakeups and stolen wakeups, and asks that the predicate be re-checked, typically in a while loop, after the wait returns. The predicate here means the condition being waited for, such as “the queue is not empty.”1
2.3 Decide Whether to Proceed From the Current Condition, Not From Why You Woke
The caller cannot tell which of the three brought it back. So the shape is: check the condition itself every time you return, and wait again if it does not hold.
The while does not prevent the spurious wakeup or the stolen wakeup itself. It is there so that, whichever of them happens, processing does not continue with the condition unsatisfied. Keep this re-check and the same-lock protection covered in Section 5, and the code handles every wakeup path correctly.
3. Why the Specification Allows It — Precise Notification Is Expensive
3.1 So That Every Operation Does Not Pay for Strict Notification
An implementation that never produces spurious wakeups is possible in theory. The reason POSIX, Windows, and C++ nonetheless allow them lies in performance and in a design in which the waiter re-checks the condition. The Rationale for POSIX’s pthread_cond_wait explains this decision as well.3
Implementing a notification that “reliably wakes exactly one thread” strictly adds extra synchronization cost to every condition variable operation, especially on multiprocessors. The scheduler sits between the notification and the wakeup, and there are interrupts and preemption as well. Allowing a rare extra wakeup and re-checking on the waiting side keeps the implementation faster.3
The re-check loop has a further benefit: it makes the intent explicit in the code and makes the code more robust. Treat a notification not as “a guarantee that the condition holds” but as a hint that the condition may have changed, and the code tolerates changes on the notifying side such as waking extra threads or waking them in a batch.3
3.2 Even With No Spurious Wakeups, Stolen Wakeups Remain
A stolen wakeup happens in the time gap between the notification and the reacquisition of the lock. Even if the producer puts one item in the queue and wakes consumer A, if consumer B takes that item before A reacquires the lock, the queue is empty by the time A returns.
sequenceDiagram
accTitle: Timeline of a stolen wakeup
accDescr: The producer puts one item in the queue and wakes waiting consumer A, but before A reacquires the lock consumer B acquires the lock and takes the one item, so the queue is empty by the time A wakes
participant A as Consumer A (waiting)
participant P as Producer
participant B as Consumer B
P->>P: Add one item to the queue
P->>A: WakeConditionVariable
Note over A: Woken, waiting to reacquire the lock
B->>B: Acquire the lock and take one item
A->>A: Reacquire the lock and return from wait
Note over A: Queue is empty (stolen)
A->>A: Re-check in the while loop and wait again
Figure 2: A stolen wakeup, in which a third thread consumes the condition during the time gap between notification and wakeup, can happen under any implementation.
As long as a third thread can take the lock first, polishing the implementation alone cannot eliminate this stolen wakeup. Even if spurious wakeups could be eliminated completely, the waiter’s loop is still needed as long as stolen wakeups exist. Given that, the design decision is to allow spurious wakeups and keep condition variables fast.
4. Which Layers It Appears in on Windows
4.1 The Re-Check Discipline Is Shared; the Reasons for Waking Are Distinguished
| API or library | Why the re-check is needed |
|---|---|
| Win32 condition variables | Spurious wakeups and stolen wakeups are documented explicitly2 |
WaitOnAddress |
Allowed to return early for reasons other than a signal to the given address7 |
C++ std::condition_variable |
A wait without a predicate can wake spuriously48 |
.NET Monitor.Wait |
Another thread can consume the condition between the wakeup and the reacquisition of the lock5 |
The official Win32 producer-consumer queue sample also writes its wait in a while loop.9
WaitOnAddress, available on Windows 8 and later, is a low-level API that waits for the value at a given address to change. As examples of returning early without a signal, the official documentation lists a low-memory condition, the abandonment of a previous wake for the same address, and running on a checked build. Its usage example is likewise a while loop that compares the value again.7
4.2 The C++ Predicate wait Performs the Loop for You
The MSVC documentation explains that wait(lock, pred) effectively executes the following.4
while (!Pred())
wait(Lck);
cppreference also states explicitly that a wait without a predicate can return spuriously. With the predicate form, this re-check loop can be left to the library.8
4.3 In C#, Focus on Stolen Wakeups and Lock Reacquisition
Monitor.Wait / Pulse use a waiting queue and a ready queue. A thread woken by Pulse / PulseAll moves to the ready queue and leaves Wait only after reacquiring the lock. That another thread can consume the condition first in that interval is the same as on Win32.510
With .NET’s Monitor.Wait, rather than assuming a reasonless wakeup of the kind condition variables have, think of it separately: the same while discipline is needed because of stolen wakeups and timeouts. The documentation likewise assumes a usage in which the thread re-evaluates the condition that caused it to wait and calls Wait again if necessary.5
flowchart TB
accTitle: Every layer requires the predicate to be re-checked
accDescr: At every layer, C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE, and the low-level WaitOnAddress, the official documentation requires the condition to be re-checked after waking
cpp["C++ std::condition_variable"] --> rule["On waking, re-check the condition (while)"]
net[".NET Monitor.Wait"] --> rule
win["Win32 CONDITION_VARIABLE"] --> rule
woa["WaitOnAddress"] --> rule
Figure 3: Whatever the language or framework, every wait-primitive layer officially requires a re-check after waking.
5. The Correct Way to Wait — Write It With while and a Predicate
The Common Shape: Check the Condition, and Proceed Only When It Holds
What you wait for is not “whether a notification was received” but shared state protected by a lock. Check and update the queue count or flag inside the same lock, wait if the condition does not hold, and re-check when you return.
flowchart TB
accTitle: Flow of a correct wait loop
accDescr: Acquire the lock and check the condition; if it does not hold, release the lock and sleep; on waking, reacquire the lock and return to the condition check. Proceed with the lock held only when the condition holds
l["Acquire the lock"] --> c{"Does the condition hold?"}
c -->|"No"| s["wait (release the lock and sleep)"]
s --> wk["Wake (reacquire the lock)"]
wk --> c
c -->|"Yes"| go["Process while still holding the lock"]
Figure 4: A correct wait is a loop, and there is no gap between checking the condition and processing (both happen while the lock is held).
With this shape, at the moment you leave the loop, you have confirmed that the condition holds while still holding the lock. You consume the state as is, so there is no gap between checking the condition and processing in which another thread can interpose.
The Basic Shape in Win32 (C)
CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0; // shared state protected by cs
// Initialize once at startup (for static initialization, cv = CONDITION_VARIABLE_INIT)
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);
// Waiter (consumer)
EnterCriticalSection(&cs);
while (queueCount == 0) { // always while, never if
SleepConditionVariableCS(&cv, &cs, INFINITE);
}
// Here the lock is held and queueCount > 0 is guaranteed
--queueCount;
LeaveCriticalSection(&cs);
// Notifier (producer)
EnterCriticalSection(&cs);
++queueCount; // update the state inside the lock
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv); // notifying after releasing the lock is fine
The distinction to keep is between updating the state and where the notification is called. ++queueCount must always happen inside the lock. WakeConditionVariable, on the other hand, can be called from either inside or outside the lock. Microsoft says it is usually better to wake after releasing the lock, to reduce context switches.1
The Basic Shape in C++ — Make the Predicate wait the Default
std::mutex m;
std::condition_variable cv;
std::queue<Item> q;
// Waiter
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !q.empty(); }); // internally while(!pred) wait
Item item = std::move(q.front());
q.pop();
lk.unlock();
// Notifier
{
std::lock_guard<std::mutex> lk(m);
q.push(std::move(item));
}
cv.notify_one();
New code should default to the predicate wait. An existing while (q.empty()) cv.wait(lk); is also a correct shape, so there is no need to rewrite it just because the loop is hand-written. The problem is if (q.empty()) cv.wait(lk);, which does not re-check.
The predicate form takes over only the loop. The discipline of protecting the shared state the predicate reads with the same mutex on the notifying side too, and of updating the state before notifying, is still required.
The Basic Shape in C#
private readonly object _gate = new();
private readonly Queue<Item> _queue = new();
// Waiter
lock (_gate)
{
while (_queue.Count == 0) // always while, never if
{
Monitor.Wait(_gate);
}
var item = _queue.Dequeue();
}
// Notifier
lock (_gate)
{
_queue.Enqueue(item);
Monitor.Pulse(_gate); // Monitor's Pulse can only be called inside the lock
}
Monitor.Wait / Pulse / PulseAll are called inside a synchronized block that holds the lock in question. Outside the lock they throw SynchronizationLockException. Do not confuse this with Win32, where the notification can be moved outside the lock.10
Waiting With a Timeout — Compute the Remaining Time From a Deadline
Passing the same timeout value every time adds to the wait each time a spurious wakeup returns. Use the shape decide the deadline first, and compute the remaining time each time you return.
ULONGLONG deadline = GetTickCount64() + timeoutMs;
EnterCriticalSection(&cs);
while (queueCount == 0) {
ULONGLONG now = GetTickCount64();
if (now >= deadline) {
break; // timeout (condition still unsatisfied)
}
if (!SleepConditionVariableCS(&cv, &cs, (DWORD)(deadline - now)) &&
GetLastError() != ERROR_TIMEOUT) {
break; // on a failure other than timeout, stop waiting and leave
}
// ERROR_TIMEOUT gets its final check in the while condition and the deadline check
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
flowchart TB
accTitle: Correct flow of a wait with a timeout
accDescr: Decide the deadline first, and each time you wake check the condition and the deadline; if the deadline has not passed, recompute the remaining time and return to the wait
d["Decide the deadline"] --> c{"Does the condition hold?"}
c -->|"Yes"| go["Proceed to processing"]
c -->|"No"| t{"Has the deadline passed?"}
t -->|"Yes"| to["Handle the timeout"]
t -->|"No"| w["Compute the remaining time and wait"]
w --> c
Figure 5: A wait with a timeout does not pass “the same wait time” again; it recomputes the remaining time from the deadline.
In C++, this processing can be left to the predicate overload of wait_until, which takes an absolute time. Because it returns the predicate’s final value even on timeout, you can decide by whether the condition ultimately held.4
6. Patterns to Avoid
6.1 Checking the Condition Only Once With if
When a spurious wakeup or a stolen wakeup happens, processing continues with the condition unsatisfied. It shows up as a rare crash or data corruption: taking from an empty queue, reading uninitialized data, a double free. Change it to the while or the predicate wait of Section 5.
6.2 Checking or Updating the Condition Outside the Lock
This one is not a matter of “waking too often” but of a lost wakeup: missing the notification and sleeping forever.
If the waiter looks at the condition outside the lock, decides “not yet,” and the notifier updates the state and notifies before the waiter enters the wait, there is no waiter at that instant. The waiter enters the wait after the notification is gone, and keeps waiting if no further notification comes.
sequenceDiagram
accTitle: Timeline of a lost wakeup
accDescr: If the waiter checks the condition outside the lock and the notifier updates the state and notifies in the gap before the wait is entered, the notification is sent to a condition variable with no waiter and vanishes, and the waiter keeps waiting for a notification that will never come
participant W as Waiter
participant N as Notifier
W->>W: Check the condition outside the lock (does not hold)
N->>N: Update the state and notify
Note over N: No waiter at this moment
W->>W: Enter wait
Note over W: The notification is already gone, so it never wakes
Figure 6: Checking the condition outside the lock lets a notification pass straight through the gap between the check and the wait: a lost wakeup.
The reason a condition variable’s wait API makes releasing the lock and entering the wait atomic is to close this gap. Keep the discipline of protecting the check and update of the condition with the same lock and entering the wait API while holding the lock, and this lost wakeup is prevented.1
6.3 Recreating a Transient Notification With a Pulse on an Event
Using CreateEvent and SetEvent is not in itself an anti-pattern. Events have legitimate uses such as the following.
| Purpose | When to use an event |
|---|---|
| Waking a single consumer | A design in which the woken consumer processes the queue until it is empty |
| Signaling a stop | Representing a stop instruction that, once raised, is never lowered, with a manual-reset event |
| Waiting together with other wait targets | Including it in WaitForMultipleObjects |
| Rendezvous across processes | A use that a condition variable, which cannot be shared between processes, cannot handle |
A condition variable is a user-mode object that cannot be shared between processes. Depending on the use, replacing the event with a condition variable is not even possible.1
What is dangerous is trying to recreate with an event the condition variable’s behavior of “waking only the threads waiting at that instant and leaving no notification state behind.” That idea leads to the PulseEvent problem that comes next.
6.4 Using PulseEvent
PulseEvent is an API that, on a manual-reset event, wakes the threads waiting at that instant and immediately returns the event to the non-signaled state. Microsoft, however, states explicitly that it is unreliable and exists mainly for backward compatibility, so new applications should not use it and should use a condition variable instead.6
The reason is that a waiting thread can be temporarily removed from the wait state by a kernel-mode APC and return to the wait after the APC completes. If PulseEvent is called during that interval, the thread is not among “the threads waiting at the moment of the call” and is not woken. Kernel APCs are internal OS behavior that the application cannot control.611
This problem is also static analysis warning C28648.12 A spurious wakeup is a problem of “waking too often”; this is a problem of “not waking when it should.” Because the notification itself is lost, wrapping the wait in a while alone cannot save you.
6.5 Notifying Without Holding the Lock, Before Updating the State
If you notify first without holding the lock and only then take the lock and update the state, the woken thread may see the old state and go back to sleep. If no notification follows the update, it stays there.
Distinguish the following two cases, however.
| Order | Result |
|---|---|
| Notify outside the lock, then take the lock and update the state | There is a gap in which the woken thread re-checks before the update and sleeps |
| Notify, update, then release, all while holding the same lock | The waiter cannot check until it reacquires the lock, so this order causes no actual harm |
Rather than having to examine the safety of the latter every time, it is clearer and safer to standardize on the order update the state inside the same lock, then notify.
7. How to Investigate When You Encounter It
7.1 Separate “Proceeds Although the Condition Does Not Hold” From “Does Not Wake”
| Symptom | What to check first | How to investigate |
|---|---|---|
| Taking from an empty queue, a crash, missing results | Whether the condition is re-checked after the wait | Search around cv.wait( without a predicate, SleepConditionVariableCS, and Monitor.Wait |
| A thread that should wake does not return, and the process hangs | Whether there is a lost wakeup or a PulseEvent |
Check each thread’s stack in a dump, and trace from the wait API it is stuck in back to the notifying side |
Finding a cv.wait(lk) without a predicate is not a problem if it is wrapped in a correct while. Collect candidates by searching, then review whether the surrounding code is only an if, and whether the condition is checked and updated under the same lock. These are places you can inspect without waiting for a reproduction.
For a hang, identify the wait site from a dump, then trace through the code who was supposed to notify, and in what order. Check for condition checks outside the lock, notifications outside the lock before the state update, and PulseEvent.
flowchart TB
accTitle: Triage flow from the symptom
accDescr: If the symptom is processing proceeding with the condition unsatisfied, search the code for waits without a predicate; if the symptom is a thread that does not wake, identify the wait site from a dump and suspect a lost wakeup or PulseEvent
s["A bug that only appears rarely"] --> a["Processing proceeds with the condition unsatisfied"]
s --> b["A thread that should wake does not"]
a --> a1["Search the code for waits without a predicate"]
b --> b1["Identify the waiting threads from a dump"]
a1 -.-> a2["Change if to while or a predicate wait"]
b1 -.-> b2["Suspect a lost wakeup or PulseEvent"]
Figure 7: Whether the symptom is “proceeding too far” or “never waking” decides both where to look and how to investigate.
7.2 Compare Before and After the Fix Under the Same Stress Conditions
To reproduce a rare bug, widen the race window and increase timing variation. Methods include using more threads than physical cores, inserting a diagnostic Sleep between the wait and the notification, and trying both debug and release builds.
When comparing to conclude “it stopped reproducing after we fixed the wait,” apply the same stress before and after the fix.
8. Summary — Checklist
A notification is a hint that the condition may have changed; the grounds for continuing are the condition itself, checked inside the lock.
| Item | Correct shape |
|---|---|
| After returning from the wait | Do not try to distinguish a genuine notification, a spurious wakeup, and a stolen wakeup; re-check the condition |
| How the wait is written | Wrap it in while. New C++ code defaults to the predicate wait(lock, pred) |
| Shared state | Protect the check and the update with the same lock, and update the state before notifying |
| Where to notify | Win32/C++ may notify after releasing the lock. C#’s Pulse is called inside the lock |
| Timeouts | Compute the remaining time from a deadline. In C++, use wait_until with a predicate |
| Use of events | Distinguish legitimate uses from transient pulses, and do not depend on PulseEvent |
Spurious wakeups are a specification that Win32, C++, and POSIX deliberately allowed. The response is discipline on the waiting side, not waiting for an OS fix or switching libraries. .NET’s Monitor.Wait is distinguished from reasonless wakeups, but performs the same re-check to cope with stolen wakeups and timeouts.
For a crash, start from “the place that proceeds even though the condition does not hold”; for a hang, from “the place that misses the notification.” Use this distinction and the checklist when reviewing existing code as well.
Related Articles
- Practical Multithreading Best Practices: C++ Edition
- Practical Multithreading Best Practices: C Edition
- Practical Multithreading Best Practices: .NET Edition
- Why You Should Prefer Event Waits over Sleep(1) on Windows
- Shared Memory Pitfalls and Practical Best Practices
- The Depths of Windows I/O (Part 2) — Synchronous and Asynchronous I/O: What OVERLAPPED Really Means
Related Consulting Areas
KomuraSoft LLC handles design reviews of multithreaded code, root-cause investigation (dump analysis) of crashes and hangs that “only reproduce occasionally,” and reworking legacy synchronization code (code that depends on events, PulseEvent, and the like) onto a condition variable basis. Starting from isolating the symptom is fine; please feel free to get in touch.
- Technical Consulting & Design Review
- Bug Investigation & Root-Cause Analysis
- Windows Application Development
- Contact Us
References
-
Microsoft Learn, Condition Variables. On a condition variable being a user-mode object that atomically releases the lock and enters the wait; on there being spurious wakeups (wakeups not tied to an explicit wake) and stolen wakeups (another thread running before the woken thread), so that the predicate should be re-checked in a while loop after returning from the wait; and on notification being possible from either inside or outside the lock, but waking after releasing the lock being better for reducing context switches. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, SleepConditionVariableCS function (synchapi.h). On atomically releasing the specified critical section and waiting on the condition variable; on the woken thread reacquiring the critical section before returning; on ERROR_TIMEOUT being returned on timeout; and on there being spurious wakeups and stolen wakeups, so that the predicate should be re-checked (typically in a while loop) after returning from the wait. ↩ ↩2
-
The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. On spurious wakeups from pthread_cond_wait / pthread_cond_timedwait being possible; on returning from the wait meaning nothing about the value of the predicate, so the predicate should be re-evaluated; and on the Rationale stating that an implementation that “wakes exactly one” can slow condition variable operations, especially on multiprocessors, and that allowing spurious wakeups forces a predicate-check loop and makes applications more robust. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, condition_variable Class. On the wait without a predicate being stated to be released by notify_one / notify_all and also to be able to wake spuriously; on the predicate wait(lock, pred) effectively executing while (!Pred()) wait(Lck);; and on wait_for / wait_until having the same property and predicate overloads. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Monitor.Wait Method. On Wait releasing the lock and entering the waiting queue; on not returning after being woken by Pulse / PulseAll until the lock is reacquired; and on the intended usage being that the woken thread re-evaluates the condition that caused it to wait and calls Wait again if necessary. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, PulseEvent function (winbase.h). On a waiting thread being able to be temporarily removed from the wait state by a kernel-mode APC and returning after the APC completes, so that the thread is not released if PulseEvent is called in that interval; and on PulseEvent therefore being unreliable, not to be used in new applications, and to be replaced by a condition variable. ↩ ↩2 ↩3
-
Microsoft Learn, WaitOnAddress function (synchapi.h). On the function that waits for the value at an address to change being guaranteed to return when signaled but also permitted to return for other reasons; on the examples of waking early being a low-memory condition, the abandonment of a previous wake for the same address, and running on a checked build; and on the value therefore needing to be compared again after return, the official sample itself being a while loop. ↩ ↩2
-
cppreference.com, std::condition_variable::wait. On the wait without a predicate being able to be released by a spurious wakeup; and on the predicate overload being equivalent to while (!pred()) wait(lock); and being defined as a loop that reacquires the lock and checks the predicate on every notification or spurious wakeup. ↩ ↩2
-
Microsoft Learn, Using Condition Variables. On the official sample that implements a producer-consumer queue with one critical section and two condition variables (BufferNotEmpty and BufferNotFull). The wait is performed inside a loop that checks the predicate. ↩
-
Microsoft Learn, Monitor.PulseAll Method. On PulseAll moving the threads in the waiting queue to the ready queue, and the next thread in the ready queue acquiring the lock when the lock is released; and on Pulse / PulseAll / Wait only being callable from inside a synchronized block. ↩ ↩2
-
Microsoft Learn, Waits and APCs. On kernel APCs executing preemptively, and the system internally interrupting and resuming the wait without returning from the wait API, so that a transient signal such as KePulseEvent can be missed in that interval. ↩
-
Microsoft Learn, C28648: PulseEvent is an unreliable function. On static analysis warning about the use of PulseEvent; on a thread that was out of the wait because of an APC not being released and being able to hang forever; and on the guidance for replacing it with SetEvent or another synchronization object. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
DllMain and the Loader Lock — The Real Reason You Are Told to "Do Nothing in DLL Initialization"
Why DllMain must not call LoadLibrary or wait on threads: the loader lock serializes DLL notifications, typical deadlocks, deferred initi...
Why Arguments Break — The Rules of Windows Command-Line Arguments
Windows passes CreateProcess a single string that the receiver splits. Covers the CommandLineToArgvW, CRT, and .NET rules, ArgumentList, ...
What Is Left After the Parent Dies — Keeping Child Processes in a Job Object
Why do SDK helpers outlive a killed UI and keep the camera or COM port? Designing child process lifetime with Job Objects, KillOnJobClose...
The Win32 Thread Pool API — Concurrency That Does Not Create Threads, with CreateThreadpoolWork
Calling CreateThread everywhere in native code? A primary-source guide to the Win32 thread pool API: the work, timer, wait, and io object...
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard IPC. Covers byte vs. message mode, servers that handle multiple clients, ACL and impe...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Bug Investigation & Long-Run Failures
Topic page for intermittent failures, communication diagnosis, long-run crashes, and failure-path test foundations.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Bug Investigation & Root Cause Analysis
We investigate difficult production issues such as intermittent failures, long-run crashes, leaks, and communication stoppages.
Frequently Asked Questions
Common questions about the topic of this article.
- Is a spurious wakeup a bug in the OS or the library?
- It is not a bug; it is behavior spelled out in the specification. Win32's SleepConditionVariableCS, C++'s std::condition_variable, and POSIX's pthread_cond_wait all have official documentation or a standard that states explicitly that a wakeup not tied to a notification can occur. An implementation that forbids it is possible in theory, but it would slow down every condition variable operation (notification on multiprocessors in particular), so the behavior is tolerated on the understanding that correctness is preserved as long as the waiter re-checks the condition. The remedy is therefore not to wait for an OS fix but to always write the wait in a while loop (or with a predicate wait).
- Does wrapping the wait in a while loop hurt performance?
- In practice the cost is negligible. All the while loop adds is one condition check each time the thread wakes, and that is a cheap comparison made while holding the lock. Spurious wakeups themselves are rare, so the extra loop iteration only happens in exceptional cases. The price of leaving an if in place, on the other hand, is a bug that only reproduces rarely, in which processing proceeds with the condition unsatisfied; there is no comparison. What actually matters for the cost of a condition variable wait is lock contention and how often you notify, not whether the while is there.
- If I use C++'s predicate wait, can I stop thinking about spurious wakeups?
- For the wait loop, yes: cv.wait(lock, pred) effectively executes while (!pred()) wait(lock); so both spurious wakeups and stolen wakeups are absorbed automatically. New C++ code should default to the predicate overload. You still have to protect updates to the shared state the predicate reads with the same mutex, and the notifier still has to update the state before calling notify. The predicate wait takes over the loop; it does not take over lock discipline.
- Does the same problem occur with C#'s Monitor.Wait?
- Yes. A thread waiting in Monitor.Wait is woken by Pulse/PulseAll and then reacquires the lock before leaving Wait, but in that interval another thread may have acquired the lock first and consumed the condition (a stolen wakeup). Microsoft's documentation is also written on the assumption that the woken thread re-evaluates the condition that caused it to wait and calls Wait again if necessary. So the basic shape in C# is also while (!condition) Monitor.Wait(gate);. That Wait/Pulse can only be called inside a lock statement is a constraint that differs from Win32.
- Are there spurious wakeups when waiting on an event with WaitForSingleObject as well?
- In an ordinary (non-alertable) wait, WAIT_OBJECT_0 is returned only when the object actually becomes signaled, so there is no reasonless wakeup of the kind condition variables have. However, the event becoming signaled and your application's condition holding are different things. In a design where several consumers are woken by the same event, the thread that takes the lock first consumes the condition, so a condition check after waking is needed after all. Also, a design that tries to reproduce a condition variable's transient notification, which wakes only the threads waiting at that instant, with an event tends to end up at PulseEvent's reliability problem, so for waiting on a state to become true within a process, a condition variable is the safe choice.