Spurious Wakeups — Why Condition Variables Wake "Without Being Notified" and How to Wait Correctly on Windows

· · Windows, Multithreading, Condition Variables, Synchronization, C++, C#, Win32 API, Troubleshooting

“We put data in the queue and wake the waiting worker thread. It had been running for six months, then one day it tried to read an empty queue and crashed.” “We’re sending the notification, but now and then a thread never wakes.” — Multithreaded rendezvous looks as if it is working, and is a breeding ground for bugs that only appear rarely. Investigations of this kind often land on code that wraps a condition variable’s wait in an if. And behind that sits the spurious wakeup — the phenomenon of returning from wait without having received a notification.

“It wakes even though nobody notified it” sounds like a defect in the implementation, but it is behaviour that Win32, C++, and POSIX all spell out in their documentation or standards, and .NET’s Monitor is designed on the assumption that “once woken, you re-check the condition”. Why is that behaviour allowed? Which layer does it happen in on Windows? And how do you write the wait so you never hit it? Aimed at developers writing business applications and equipment-control software on Windows, this article unpacks what a spurious wakeup really is from primary sources, and boils the correct wait down into Win32 (C), C++, and C#.

1. The Bottom Line First

  • A condition variable’s wait can return even when no notification has arrived. Win32’s official documentation states that condition variables are subject to spurious wakeups (wakeups not tied to an explicit wake) and to stolen wakeups (another thread consuming the condition before the woken thread does).1
  • So you must always write the wait as “a while loop plus a re-check of the condition”. Code that checks once with if and then waits looks as if it is working, and harbours a bug that only reproduces rarely.12
  • This is not a Windows-specific quirk; POSIX and the C++ standard say the same thing. An implementation that “absolutely never spuriously wakes” would slow down every condition-variable operation, so the wakeup is allowed on the assumption that the waiter will re-check.34
  • In C++, the predicate form wait(lock, pred) has the library perform the loop for you. That form effectively runs while (!pred()) wait(lock);. It is the default for new code.5
  • C#’s Monitor.Wait needs the same discipline. The condition can be consumed in the interval between being woken and reacquiring the lock, so you re-check the condition in a while and go back to Wait.6
  • Update and check the condition under the same lock. If you look at the condition outside the lock and then enter wait, a notification can pass through the gap — a lost wakeup.1
  • Do not recreate a condition variable’s “wake whoever is waiting right now” transient notification with a pulse on an event. PulseEvent in particular can miss the notification in the instant a kernel-mode APC briefly lifts the wait, and Microsoft itself says, in so many words, “it is unreliable, do not use it, use a condition variable instead”.7

What follows walks through the mechanisms that support this conclusion, in order.

2. What a Spurious Wakeup Is — Waking Up Does Not Mean the Condition Holds

A condition variable is a synchronisation primitive for “putting a thread to sleep until some condition holds, and having it woken when it does”. On Win32 that is the CONDITION_VARIABLE structure together with SleepConditionVariableCS / SleepConditionVariableSRW (wait) and WakeConditionVariable / WakeAllConditionVariable (notify). The wait API atomically releases the lock you hold (a critical section or an SRW lock) and goes to sleep, and on wakeup it reacquires the lock before returning.1

The question is what the fact of “having returned from wait” actually means. Naively you want to think “a notification arrived = the condition holds”, but in reality there are three cases in which wait returns.

Case Notification Condition at return
Genuine wakeup Yes Often satisfied, but not guaranteed
Spurious wakeup None addressed to you Still unsatisfied
Stolen wakeup Yes Another thread consumed it first; unsatisfied
Three cases in which wait returnsA condition variable wait can return 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 every case needs the condition re-checkedReturned from waitGenuine notificationSpurious wakeup (no notification)Stolen wakeup (condition already consumed)Re-check the condition, then proceed

Figure 1: There are three paths back from wait, and the caller cannot tell which one it took, so you must always re-check the condition.

A spurious wakeup is this second case — the phenomenon of the wait API returning without being tied to an explicit notification that was meant to wake you. It is not limited to situations in which WakeConditionVariable has never been called anywhere in the system. For example, under a high load where notifications arrive in a short burst, the implementation may wake extra waiting threads in a batch, and from the side that has no corresponding notification that too is a spurious wakeup. Microsoft Learn’s condition-variable page says this plainly: “Condition variables are subject to spurious wakeups (those not associated with an explicit wake) and stolen wakeups (another thread manages to run before the woken thread). Therefore, you should recheck a predicate (typically in a while loop) after a wait operation returns.”1

The important point is that the caller cannot tell which of the three cases it returned through. If you cannot tell, there is only one strategy available: every time you return, check the condition you were waiting for itself, and go back to sleep if it does not hold. That is the real content of the iron rule “wrap wait in a while”. Put the other way around, as long as you keep that rule, the code is correct no matter which of the three cases woke you.

3. Why the Specification Allows It — Precise Notification Is Expensive

“Waking without being notified is just sloppy implementation, isn’t it?” is a fair question. In fact it is theoretically possible to build an implementation that never spuriously wakes. Even so, POSIX, Windows, and the C++ standard all came down on the “it can happen” side. The reason is stated frankly in the Rationale for pthread_cond_wait in POSIX (The Open Group Base Specifications).3

The first reason is performance. Trying to implement notification that “reliably wakes exactly one thread” strictly, especially on multiprocessors, adds extra synchronisation cost to every condition-variable operation. A scheduler sits between notification and wakeup, and depending on the timing of interrupts and preemption you cannot avoid “a different thread running before the one you meant to wake”. Paying everyone the cost of sealing that off completely is worse, for keeping condition variables fast, than accepting that “you may occasionally wake extra”.

The second reason is the observation that this trade-off does not break applications — it actually makes them more robust. Because spurious wakeups are allowed, correct code always writes a loop that checks the predicate (the condition being waited for). POSIX’s Rationale says that forcing this loop makes the code self-documenting and more robust.3 Once the loop is there, the meaning of a notification is demoted from “a guarantee that the condition holds” to “a hint that the condition may have changed”, and the waiting side becomes tolerant of modest design changes on the notifier (waking too many, waking in a batch, and so on).

Stolen wakeups are a still more structural matter. There is always a time gap between the notifier calling WakeConditionVariable and the woken thread reacquiring the lock and returning from wait. If a third thread can take the lock in that interval, it can consume the condition (the contents of the queue, and so on) first. That is a gap that no amount of polishing the implementation can erase, because it comes from the shape of the condition-variable tool itself.

Timeline of a stolen wakeupThe 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 wakesConsumer BProducerConsumer A (waiting)Consumer BProducerConsumer A (waiting)Woken, waiting to reacquire the lockQueue is empty (stolen)Add one item to the queueWakeConditionVariableAcquire the lock and take one itemReacquire the lock and return from waitRe-check in the while loop and wait again

Figure 2: A “stolen wakeup”, in which a third thread consumes the condition in the time gap between notification and wakeup, can happen under any implementation.

In other words, even if the OS completely eradicated spurious wakeups, as long as stolen wakeups exist you still cannot write “I woke = the condition holds”. The waiter’s re-check loop is required anyway, and given that, it is cheaper to allow spurious wakeups and keep the implementation fast — that is the design judgement condition variables have carried for decades.

4. Which Layers It Shows Up In on Windows

This property shows its face whichever layer of Windows synchronisation primitive you use. To get a feel for the fact that you cannot escape it no matter which layer’s API you write against, we will look at the representative layers.

Win32 condition variables (CONDITION_VARIABLE) are, as already noted, documented on SleepConditionVariableCS / SleepConditionVariableSRW as subject to both spurious wakeups and stolen wakeups, and you are required to re-check the predicate in a while loop.2 The official usage sample (a producer–consumer queue) also writes the wait inside a while loop.8

The still lower-level WaitOnAddress is a more primitive wait API than a condition variable: “wait until the value at a given address changes” (Windows 8 and later). Even this near-bottom-layer API has documentation that states “it is guaranteed to return when the address is signaled, but it is also permitted to return for other reasons”, and lists as examples of waking early a low-memory condition, abandoning a previous wake for the same address, and running a checked build. That is why the documentation’s own usage sample is in the form of “a while loop that compares the value again”.9

C++’s std::condition_variable is the same. MSVC’s documentation says of the predicate-less wait that it “blocks until signaled by a call to notify_one / notify_all. It may also wake spuriously”, and explains that the predicate form wait(lock, pred) effectively runs the following code.5

while (!Pred())
    wait(Lck);

In other words, the predicate-form wait recommended in C++ is nothing other than the library taking “wrap it in a while”, as this article describes, off your hands. cppreference likewise states that the predicate-less wait can be unblocked spuriously.4

.NET’s Monitor.Wait / Pulse has its own queue structure of a waiting queue and a ready queue, but the discipline does not change. A thread woken by Pulse / PulseAll moves to the ready queue and returns from Wait in the order it can reacquire the lock. Another thread being able to consume the condition in the interval before the lock is reacquired is the same as on Win32, and the documentation too is written on the assumption that “the woken thread re-evaluates the condition that caused it to enter the wait, and calls Wait again if necessary”.610

Every layer requires the predicate to be re-checkedOfficial documentation requires the condition to be re-checked after wakeup at every layer — C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE, and the low-level WaitOnAddressC++ std::condition_variableOn wakeup, re-check the condition (while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

Figure 3: Change the language or the framework and the official requirement is still the same at every wait-primitive layer: re-check after you wake.

5. The Correct Way to Wait — Write It with while and a Predicate

From here on, implementation. There are only three principles.

  1. Hold what you wait for as state (a predicate), not as a “notification”. The condition is shared state protected by a lock — “is the queue non-empty?”, “is the flag set?” — not “was I woken?”.
  2. Always place wait inside a while loop on the condition. Each time you wake, check the condition, and go back to sleep if it does not hold.
  3. Update and check the condition under the same lock. The notifier updates the state and then notifies.
Flow of a correct wait loopAcquire the lock and check the condition; if it does not hold, release the lock and sleep; on wakeup reacquire the lock and return to the condition check. Proceed with the lock held only when the condition holdsNoYesAcquire the lockIs the condition satisfied?wait (release the lock and sleep)Wake (reacquire the lock)Proceed 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).

This shape has an easy-to-miss benefit. The moment you leave the while loop, it is established, while you still hold the lock, that “the condition holds”. The loop that defends against spurious wakeups is, as-is, a guarantee that there is no race-condition gap between checking the condition and processing it.

The Basic Shape in Win32 (C)

CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
int queueCount = 0;   // shared state protected by cs

// Initialise once at startup (for static initialisation, 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 under the lock
LeaveCriticalSection(&cs);
WakeConditionVariable(&cv);                      // notifying after releasing the lock is fine

You can call the notification (WakeConditionVariable) from inside the lock or from outside it, but the documentation says that waking after releasing the lock is usually better, to reduce context switches.1 On the other hand, the state update itself (++queueCount) must always happen under the lock. Do not confuse the two.

The Basic Shape in C++ — Make 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();

Because the predicate-form wait performs the loop for you, a hand-written while is unnecessary. When you fix existing code that still has a hand-written loop, while (q.empty()) cv.wait(lk); is a correct shape, so there is no need to rush to rewrite it. The only incorrect form is if (q.empty()) cv.wait(lk);.

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.Pulse can only be called inside the lock
}

Monitor.Wait / Pulse / PulseAll can only be called from inside a lock (lock block), which differs from Win32. Calling them outside the lock throws SynchronizationLockException.10

Waiting with a Timeout — Compute Remaining Time from a Deadline

When you wait with a timeout, passing “the same timeout value” on every loop iteration stretches the wait each time a spurious wakeup happens. The correct shape is to fix the deadline first and recompute the remaining time.

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 failure other than timeout, stop waiting and leave
    }
    // Confirm ERROR_TIMEOUT finally via the while condition and the deadline check
}
BOOL ready = (queueCount > 0);
if (ready) { --queueCount; }
LeaveCriticalSection(&cs);
Correct flow of a wait with a timeoutFix the deadline first, and each time you wake check the condition and the deadline; if there is still time, recompute the remaining time and return to the waitYesNoYesNoFix the deadlineIs the condition satisfied?Proceed to processingHas the deadline passed?Handle the timeoutCompute remaining time and wait

Figure 5: A wait with a timeout does not pass “the same wait duration” again; it recomputes remaining time from a deadline.

In C++, you can leave this calculation, deadline and all, to the wait_until (absolute time) plus predicate overload. Even when it returns on timeout it gives you the predicate’s final value, so you can also decide “did we time out, or did we make it?” on the predicate.5

6. A Catalogue of Patterns to Avoid

Checking only once with if. This is the star of the article. The moment a spurious wakeup or a stolen wakeup happens, processing proceeds with the condition unsatisfied. Taking from an empty queue, touching uninitialised data, a double free — the symptom becomes “a crash or data corruption that only appears occasionally”.

Checking or updating the condition outside the lock. If the waiter looks at the condition outside the lock, decides “not yet”, and in the gap before entering wait the notifier updates the state and sends a notification, the notification is fired at a condition variable with no waiter and vanishes. The waiter then enters wait and keeps waiting for a notification that will never come again. That is a lost wakeup, the mirror image of a spurious wakeup. The reason a condition variable’s wait API is designed to “atomically release the lock and go to sleep” is precisely to close this gap.1 It does not happen as long as you keep lock discipline.

Timeline of a lost wakeupIf the waiter checks the condition outside the lock and the notifier updates the state and notifies in the gap before 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 comeNotifierWaiterNotifierWaiterNo waiter at this momentThe notification is already gone and it never wakesCheck the condition outside the lock (unsatisfied)Update the state and notifyEnter wait

Figure 6: If you check the condition outside the lock, the notification slips through the gap between the check and wait — a “lost wakeup”.

Recreating a condition variable’s “transient notification” with a pulse on an event. Events themselves (CreateEvent + SetEvent) are not an anti-pattern. A wake signal in a setup where a single consumer processes the queue until it is empty, or a stop instruction that once raised is never lowered (a manual-reset event), are correct uses of an event; and when you want to join it with other wait targets via WaitForMultipleObjects, or to cross a process boundary, a condition variable — a user-mode object that cannot be shared across processes — is the one that cannot be used.1 What is dangerous is trying to recreate, with event operations, a condition variable’s transient notification that “wakes only the threads waiting at that instant and leaves no state behind”. That idea almost always leads to the next item, PulseEvent.

Using PulseEvent. It is an API that, on a manual-reset event, “wakes everyone currently waiting and immediately returns the event to the non-signaled state”, but Microsoft itself states in the documentation that “this function is unreliable and should not be used. It exists mainly for backward compatibility. Use a condition variable instead.” 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 in that brief interval, that thread is not included among “those who were waiting at the moment it was called” and is not woken.7 Kernel APCs are something the OS uses internally; the app cannot control them.11 This problem is also a static-analysis warning (C28648).12 If a spurious wakeup is the problem of “waking extra”, this is the problem of “oversleeping when you should have woken”, and a while loop cannot save you — because the notification itself has been lost.

Sending only the notification first, without holding the lock, before updating the state. Calling WakeConditionVariable while the state is still stale, and only then taking the lock and updating the state — in that order, the woken thread still sees the condition unsatisfied when it checks, and goes back to sleep. If no further notification comes, it stays there. Note that if you write “notify → update → release” while still holding the same lock, there is no real harm, because the waiter cannot check the condition until it reacquires the lock. Even so, so that readers do not have to verify this safety condition every time, it is safer to standardise on the order “update the state under the lock, and notify after that”.

7. How to Investigate When You Encounter It

Bugs involving spurious wakeups are characterised by “only appearing rarely”. Working backwards from the symptom, they split into the following two families.

Family 1: Processing proceeds with the condition unsatisfied. An exception or crash from taking from an empty queue, missing results, and so on. Suspect a wait without a predicate. You can comb this out mechanically in code review — search for places where cv.wait( has only one argument, and places where SleepConditionVariableCS / Monitor.Wait is wrapped in if rather than while. This check does not require waiting for a reproduction, and is the highest-leverage move you have.

Family 2: A thread that should wake does not (a hang). Suspect a lost wakeup (checking the condition outside the lock, or notifying outside the lock before updating the state) and PulseEvent. Take a dump from the hung process and look at each thread’s stack, and you can identify which thread is stuck in which wait API. From there, chase through the code “who was supposed to send that notification, and in what order”.

Triage flow from the symptomIf processing proceeds with the condition unsatisfied, comb out waits without a predicate by searching the code; if a thread does not wake, identify the wait site from a dump and suspect a lost wakeup or PulseEventA bug that only appears rarelyProcessing proceeds with the condition unsatisfiedA thread that should wake does notSearch the code for waits without a predicateIdentify waiting threads from a dumpChange if to while, or use predicate waitSuspect a lost wakeup or PulseEvent

Figure 7: Whether the symptom is “proceeding too far” or “never waking” splits both what you suspect and how you investigate.

If you want to reproduce it, the standard move is to widen the race window. Increase timing jitter by using more threads than physical cores, inserting a deliberate Sleep between wait and notify, and running both debug and release builds. When you confirm that “it stopped reproducing after we fixed the predicate-less wait”, compare under the same stress.

8. Summary — A Checklist

  • The paths back from wait are three — genuine notification, spurious wakeup, and stolen wakeup — and the caller cannot tell them apart. So always write the wait as a while loop on the condition.
  • A spurious wakeup is behaviour that Win32, C++, and POSIX deliberately allowed as a trade-off against performance, and it will not go away with an OS fix or a library swap. .NET’s Monitor.Wait is not assumed to wake for no reason, but because stolen wakeups and timeouts exist, the same while discipline is still required.
  • In C++, default to the predicate form wait(lock, pred). The library performs the loop.
  • Update and check the condition under the same lock. Send the notification “after updating the state”. Win32/C++ notification may happen after releasing the lock; C#’s Pulse is inside the lock only.
  • For a wait with a timeout, fix a deadline and recompute remaining time. In C++, wait_until plus a predicate.
  • Do not recreate a condition variable’s transient notification with a pulse on an event. PulseEvent in particular is something official documentation states, in so many words, “do not use, use a condition variable instead”. Events themselves remain the right tool for a stop instruction, joining with WaitForMultipleObjects, and cross-process synchronisation.
  • In review, search mechanically for “predicate-less wait” and “if + wait”. You can kill a rarely-reproducing bug without waiting for a reproduction.

A spurious wakeup, contrary to the oddness of the name, condenses to a one-line keyword for the fix — change if to while. And behind that one line lies the design idea of the condition-variable tool: “precise notification is expensive, so checking is the waiter’s responsibility”. Understand it as a mechanism and you should be able to apply the same discipline without hesitation when the language or the framework changes.

KomuraSoft LLC handles multithread design reviews, root-cause investigation (dump analysis) of crashes and hangs that “only reproduce occasionally”, and migrating legacy synchronisation code (event- and PulseEvent-dependent, and the like) onto a condition-variable base. Starting from triaging the symptom is fine — please feel free to get in touch.

References

  1. Microsoft Learn, Condition Variables. On a condition variable being a user-mode object that atomically releases a lock and enters a 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 after returning from a wait you should re-check the predicate in a while loop; and on notification being possible from inside or outside the lock, but waking after releasing the lock being better for reducing context switches.  2 3 4 5 6 7 8

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). On atomically releasing a specified critical section and waiting on a 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 after returning from a wait you should re-check the predicate (typically in a while loop).  2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. On spurious wakeups from pthread_cond_wait / pthread_cond_timedwait being able to occur; on returning from wait meaning nothing about the predicate’s value, 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. cppreference.com, std::condition_variable::wait. On the predicate-less wait being able to be unblocked by a spurious wakeup; and on the predicate overload being equivalent to while (!pred()) wait(lock); and defined as a loop that reacquires the lock and checks the predicate on each notification or spurious wakeup.  2

  5. Microsoft Learn, condition_variable Class. On the predicate-less wait being stated to unblock on notify_one / notify_all and also to be able to wake spuriously; on the predicate form wait(lock, pred) effectively running while (!Pred()) wait(Lck);; and on wait_for / wait_until having the same property and a predicate overload.  2 3

  6. 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 enter the wait and calls Wait again if necessary.  2

  7. 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 if PulseEvent is called in that interval the thread is not released; and on PulseEvent therefore being unreliable and not to be used in new applications, a condition variable being used instead.  2

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

  9. Microsoft Learn, WaitOnAddress function (synchapi.h). On the function that waits for an address’s value to change being guaranteed to return when signaled but also permitted to return for other reasons; on examples of waking early including a low-memory condition, abandoning a previous wake for the same address, and running a checked build; and on therefore needing to compare the value again after return, the official sample itself being a while loop. 

  10. Microsoft Learn, Monitor.PulseAll Method. On PulseAll moving threads from the waiting queue to the ready queue, and the next thread on the ready queue acquiring the lock when the lock is released; and on Pulse / PulseAll / Wait only being callable from inside a synchronisation block.  2

  11. Microsoft Learn, Waits and APCs. On kernel APCs executing preemptively, and the system internally interrupting and resuming a wait without returning from the wait API, so that a transient signal such as KePulseEvent can be missed in that interval. 

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. On static analysis warning on 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 guidance for replacing it with SetEvent or another synchronisation object. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Is a spurious wakeup a bug in the OS or the library?
No — it is behaviour 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 forbade it is theoretically possible, but it would slow down every condition-variable operation (especially notification on multiprocessors), so the trade-off is to allow it on the understanding that "correctness is preserved if the waiter re-checks the condition". The remedy is therefore not to wait for an OS fix, but to always write wait inside a while loop (or use a predicate-form wait).
Does wrapping wait in a while loop hurt performance?
In practice the cost is negligible. All the while loop adds is one extra condition check each time you wake, and that is a cheap comparison while you already hold the lock. Spurious wakeups themselves are rare, so the extra loop iteration only happens in exceptional cases. The cost of leaving the check as an if, 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 dominates condition-variable wait cost is lock contention and how often you notify, not whether the while is there.
If I use C++'s predicate-form wait, can I forget about spurious wakeups?
For the wait loop, yes: cv.wait(lock, pred) is effectively 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 that state before calling notify. Predicate wait takes the loop off your hands; it does not take lock discipline off your hands.
Does the same problem happen with C#'s Monitor.Wait?
Yes. A thread waiting in Monitor.Wait is woken by Pulse/PulseAll and then reacquires the lock before returning from Wait, but in that interval another thread may have acquired the lock first and consumed the condition (a stolen wakeup). Microsoft's documentation is 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);. One constraint that differs from Win32 is that you can only call Wait/Pulse from inside a lock statement.
Do spurious wakeups also happen when you wait on an event with WaitForSingleObject?
In an ordinary (non-alertable) wait, WAIT_OBJECT_0 is returned only when the object actually becomes signaled; there is no "reasonless wakeup" of the kind condition variables have. That said, "the event became signaled" and "your application's condition holds" are different things. If several consumers are woken by the same event, the thread that takes the lock first consumes the condition, so you still need to re-check the condition after waking. Designs that try to recreate a condition variable's "wake only whoever is waiting at that instant" transient notification with an event also tend to run into PulseEvent's reliability problem, so for waiting on a condition inside a process, a condition variable is the safer tool.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog