Spurious wakeup — condition variable "बिना notification के" क्यों जागती है और Windows पर सही wait कैसे करें
· अद्यतन तिथि: · Go Komura · Windows, multithreading, condition variable, synchronization, C++, C#, Win32 API, troubleshooting
संशोधन इतिहास (पहला संस्करण, 22 Aug 2026 को प्रकाशित)
- पहला प्रकाशन
इस लेख का हवाला दें(DOI: 10.5281/zenodo.22176637)
यह लेख Zenodo पर संग्रहीत है। नीचे वह DOI है जो हमेशा नवीनतम संस्करण की ओर ले जाता है, और वह DOI भी जो आपके पढ़े जा रहे संस्करण से बँधा है।
Go Komura (2026). Spurious wakeup — condition variable "बिना notification के" क्यों जागती है और Windows पर सही wait कैसे करें. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22176637 https://comcomponent.com/hi/blog/windows-condition-variable-spurious-wakeup/
- DOI (नवीनतम संस्करण)
- 10.5281/zenodo.22176637
- DOI (यह संस्करण)
- 10.5281/zenodo.22176638
“हम queue में data रखते हैं और wait कर रहे worker thread को जगाते हैं। छह महीने चल रहा था, फिर एक दिन खाली queue पढ़ने की कोशिश कर crash हो गया।” “हम notification भेज रहे हैं, पर कभी-कभी thread जागता ही नहीं।” — Multithreaded rendezvous ऐसा लगता है मानो काम कर रहा हो, और दुर्लभ दिखने वाले bug का प्रजनन स्थल है। इस तरह की investigations अक्सर उस code पर उतरती हैं जो condition variable की wait को if में लपेटता है। और उसके पीछे बैठता है spurious wakeup — notification पाए बिना wait से लौटने की घटना।
“कोई notify किए बिना जागता है” implementation का दोष लगता है, पर यह व्यवहार Win32, C++, और POSIX सबके documents या standards में लिखा है, और .NET का Monitor इस धारणा पर design है कि “एक बार जागने पर, condition फिर जाँचें”। वह व्यवहार अनुमति क्यों है? Windows पर वह किस layer में होता है? और wait कैसे लिखें कि कभी न लगें? Windows पर business ऐप और device-control software लिखने वाले developers के लिए, यह लेख primary sources से spurious wakeup वास्तव में क्या है खोलता है, और सही wait को Win32 (C), C++, तथा C# में उबालता है।
1. निष्कर्ष पहले
- Condition variable की
waitnotification आए बिना भी लौट सकती है। Win32 का official document कहता है कि condition variables spurious wakeups (explicit wake से न बँधे जाग) और stolen wakeups (जागे thread से पहले दूसरे thread का condition खाना) के अधीन हैं।1 - इसलिए wait हमेशा “while loop प्लस condition की re-check” लिखनी चाहिए। जो code एक बार
ifसे जाँच कर फिरwaitकरता है ऐसा लगता है मानो काम कर रहा हो, और दुर्लभ-reproduce bug पालता है।12 - यह Windows-specific विचित्रता नहीं; POSIX और C++ standard वही कहते हैं। “बिल्कुल कभी spurious न जागे” वाली implementation हर condition-variable operation धीमा करेगी, इसलिए जाग इस धारणा पर अनुमति है कि waiter फिर जाँचेगा।34
- C++ में, predicate रूप
wait(lock, pred)library से loop करवाता है। वह रूप effectivelywhile (!pred()) wait(lock);चलाता है। नए code के लिए यही default है।5 - C# के
Monitor.Waitको वही discipline चाहिए। जागने और lock पुनः लेने के अंतराल में condition खाई जा सकती है, इसलिए conditionwhileमें फिर जाँचें औरWaitपर लौटें।6 - Condition update और check एक ही lock के अधीन करें। यदि lock के बाहर condition देखकर
waitमें प्रवेश करें, notification अंतराल से निकल सकती है — lost wakeup।1 - Condition variable की “अभी wait कर रहे को जगाओ” transient notification event पर pulse से न दोहराएँ। खासकर
PulseEventउस क्षण notification चूक सकता है जब kernel-mode APC wait संक्षेप में उठाए, और Microsoft स्वयं साफ़ शब्दों में कहता है, “यह unreliable है, इस्तेमाल न करें, इसके बजाय condition variable इस्तेमाल करें”।7
आगे इस निष्कर्ष को सहारा देने वाले mechanism क्रम से चलते हैं।
2. Spurious wakeup क्या है — जागना condition पूरी होने का अर्थ नहीं
Condition variable “thread को तब तक सुलाने का synchronization primitive है जब तक कोई condition पूरी न हो, और होने पर जगाने” के लिए है। Win32 पर वह CONDITION_VARIABLE structure के साथ SleepConditionVariableCS / SleepConditionVariableSRW (wait) और WakeConditionVariable / WakeAllConditionVariable (notification) है। Wait API आपके पकड़े lock (critical section या SRW lock) को atomically छोड़ती और सो जाती है, और जागने पर लौटने से पहले lock पुनः लेती है।1
प्रश्न है “wait से लौटना” वास्तव में क्या अर्थ रखता है। भोलेपन से सोचना चाहते हैं “notification आई = condition पूरी है”, पर वास्तव में तीन स्थितियाँ हैं जिनमें wait लौटती है।
| स्थिति | Notification | लौटने पर condition |
|---|---|---|
| वास्तविक जाग | हाँ | अक्सर पूरी, पर गारंटी नहीं |
| Spurious wakeup | आपके नाम कोई नहीं | अभी अधूरी |
| Stolen wakeup | हाँ | दूसरे thread ने पहले खा ली; अधूरी |
flowchart TB
accTitle: वे तीन स्थितियाँ जिनमें wait लौटती है
accDescr: Condition variable wait न केवल वास्तविक notification से लौट सकती है बल्कि बिना notification वाले spurious wakeup से और उस stolen wakeup से भी जहाँ notification आई पर condition पहले खा ली गई, इसलिए हर स्थिति में condition फिर जाँचनी पड़ती है
w["wait से लौटे"] --> a["वास्तविक notification"]
w --> b["Spurious wakeup(कोई notification नहीं)"]
w --> c["Stolen wakeup(condition पहले खा ली)"]
a --> r["Condition फिर जाँचें, फिर आगे बढ़ें"]
b --> r
c --> r
चित्र 1: wait से वापस तीन paths हैं, और caller नहीं बता सकता कौन सा लिया, इसलिए condition हमेशा फिर जाँचनी चाहिए।
Spurious wakeup यह दूसरी स्थिति है — wait API के आपको जगाने के लिए लिखी explicit notification से न बँधे लौटने की घटना। यह उन स्थितियों तक सीमित नहीं जिनमें सिस्टम में कहीं WakeConditionVariable कभी न बुलाया गया हो। उदाहरण के लिए, high load पर जहाँ notifications छोटी फुहार में आएँ, implementation extra wait threads batch में जगा सकती है, और जिस पक्ष के पास matching notification नहीं उसके लिए वह भी spurious wakeup है। Microsoft Learn का condition-variable पृष्ठ यह साफ़ कहता है: “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
महत्वपूर्ण बिंदु यह है कि caller नहीं बता सकता तीन में से किस स्थिति से लौटा। यदि नहीं बता सकते, तो केवल एक रणनीति उपलब्ध है: हर बार लौटने पर, जिस condition की wait कर रहे थे उसे स्वयं जाँचें, और यदि पूरी न हो तो फिर सो जाएँ। यही लौह नियम “wait को while में लपेटें” की वास्तविक सामग्री है। उलटा कहें, जब तक वह नियम रखें, तीन में से कोई भी स्थिति जगाए code सही है।
3. Spec इसे क्यों अनुमति देता है — सटीक notification महँगी है
“बिना notification जागना तो ढीली implementation है, है न?” उचित प्रश्न है। वास्तव में कभी spurious न जागने वाली implementation बनाना theoretically संभव है। फिर भी, POSIX, Windows, और C++ standard सब “हो सकता है” पक्ष पर आए। कारण POSIX (The Open Group Base Specifications) में pthread_cond_wait के Rationale में खुलकर लिखा है।3
पहला कारण performance है। “विश्वसनीय रूप से ठीक एक thread जगाओ” notification सख्ती से लागू करने की कोशिश, खासकर multiprocessor पर, हर condition-variable operation पर extra synchronization cost जोड़ती है। Notification और जागने के बीच scheduler बैठता है, और interrupt तथा preemption के समय पर आप “जिसे जगाना था उससे पहले दूसरा thread चलना” नहीं बचा सकते। उसे पूरी तरह सील करने की cost सबको चुकाना, condition variable तेज़ रखने के लिए, इससे बदतर है कि स्वीकार करें “कभी-कभी extra जगा सकते हैं”।
दूसरा कारण यह अवलोकन है कि यह समझौता ऐप नहीं तोड़ता — वास्तव में उन्हें अधिक मज़बूत बनाता है। क्योंकि spurious wakeup अनुमति हैं, सही code हमेशा predicate (जिस condition की wait है) जाँचने वाला loop लिखता है। POSIX का Rationale कहता है कि इस loop को बाध्य करना code self-documenting और अधिक मज़बूत बनाता है।3 एक बार loop हो, notification का अर्थ “condition पूरी होने की गारंटी” से गिरकर “संकेत कि condition बदल गई हो सकती है” हो जाता है, और wait पक्ष notify करने वाले के मामूली design परिवर्तन (बहुत जगाना, batch में जगाना, आदि) सहने लगता है।
Stolen wakeup और भी अधिक structural बात हैं। Notify करने वाले के WakeConditionVariable बुलाने और जागे thread के lock पुनः लेकर wait से लौटने के बीच हमेशा समय अंतराल होता है। यदि तीसरा thread उस अंतराल में lock ले सके, वह condition (queue की सामग्री, आदि) पहले खा सकता है। वह अंतराल कोई भी implementation चमकाने से मिट नहीं सकता, क्योंकि वह condition-variable उपकरण के रूप से आता है।
sequenceDiagram
accTitle: Stolen wakeup की timeline
accDescr: Producer queue में एक item रखता और wait कर रहे consumer A को जगाता है, पर A lock पुनः लेने से पहले consumer B lock ले एक item ले लेता है, इसलिए A के जागने तक queue खाली है
participant A as Consumer A(wait)
participant P as Producer
participant B as Consumer B
P->>P: Queue में एक item जोड़ें
P->>A: WakeConditionVariable
Note over A: जागा, lock पुनः लेने की wait
B->>B: Lock लें और एक item लें
A->>A: Lock पुनः लें और wait से लौटें
Note over A: Queue खाली(stolen)
A->>A: While loop में फिर जाँचें और फिर wait
चित्र 2: “Stolen wakeup”, जिसमें तीसरा thread notification और जागने के समय अंतराल में condition खाता है, किसी भी implementation के अधीन हो सकता है।
यानी, भले OS spurious wakeup पूरी तरह मिटा दे, जब तक stolen wakeup हैं आप फिर भी “मैं जागा = condition पूरी है” नहीं लिख सकते। Waiter का re-check loop वैसे भी चाहिए, और उसे देखते हुए, spurious wakeup अनुमति देकर implementation तेज़ रखना सस्ता है — यही design decision condition variable दशकों से ढोती हैं।
4. Windows पर यह किन layers में दिखता है
यह गुण Windows synchronization primitives की जिस layer का API लिखें उस पर अपना चेहरा दिखाता है। इस तथ्य का अहसास पाने के लिए कि किसी भी layer से नहीं बच सकते, प्रतिनिधि layers देखेंगे।
Win32 condition variable (CONDITION_VARIABLE) पहले ही नोट किए अनुसार SleepConditionVariableCS / SleepConditionVariableSRW पर spurious wakeup और stolen wakeup दोनों के अधीन documented हैं, और predicate while loop में फिर जाँचनी आवश्यक है।2 Official usage sample (producer–consumer queue) भी wait while loop के भीतर लिखता है।8
और निचली WaitOnAddress condition variable से अधिक primitive wait API है: “दिए address पर मान बदलने तक wait” (Windows 8 और बाद)। यहाँ तक यह लगभग-नीचे की layer API document कहती है “address signaled होने पर लौटना guaranteed है, पर अन्य कारणों से लौटना भी अनुमति है”, और early wake के उदाहरणों में low-memory स्थिति, उसी address के लिए पिछला जाग त्यागना, और checked build चलाना गिनती है। इसीलिए document का अपना usage sample “मान फिर compare करने वाले while loop” के रूप में है।9
C++ की std::condition_variable वही है। MSVC का document predicate-रहित wait के बारे में कहता है कि वह “notify_one / notify_all call से signaled होने तक block करती है। वह spurious भी जाग सकती है”, और समझाता है कि predicate रूप wait(lock, pred) effectively निम्नलिखित code चलाता है।5
while (!Pred())
wait(Lck);
यानी, C++ में recommended predicate-रूप wait और कुछ नहीं बल्कि library का “while में लपेटें”, जैसा यह लेख वर्णन करता है, आपके हाथ से लेना है। cppreference भी कहती है कि predicate-रहित wait spurious unblock हो सकती है।4
.NET का Monitor.Wait / Pulse अपनी wait queue और ready queue की queue structure रखता है, पर discipline नहीं बदलता। Pulse / PulseAll से जागा thread ready queue पर जाता है और lock पुनः ले सकने के क्रम में Wait से लौटता है। Lock पुनः लेने से पहले दूसरे thread का condition खा सकना Win32 जैसा ही है, और document भी इस धारणा पर लिखा है कि “जागा thread उस condition का re-evaluate करता है जिसके कारण wait में आया, और यदि आवश्यक हो तो Wait फिर बुलाता है”।610
flowchart TB
accTitle: हर layer predicate फिर जाँचने की माँग करती है
accDescr: Official documents हर layer पर जागने के बाद condition फिर जाँचने की माँग करते हैं — C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE, और low-level WaitOnAddress
cpp["C++ std::condition_variable"] --> rule["जागने पर condition फिर जाँचें(while)"]
net[".NET Monitor.Wait"] --> rule
win["Win32 CONDITION_VARIABLE"] --> rule
woa["WaitOnAddress"] --> rule
चित्र 3: भाषा या framework बदलें और official requirement हर wait-primitive layer पर वही है: जागने के बाद फिर जाँचें।
5. सही wait — while और predicate से लिखें
यहाँ से, implementation। केवल तीन सिद्धांत हैं।
- जिसकी wait करते हैं उसे “notification” नहीं, state (predicate) के रूप में रखें। Condition lock से सुरक्षित shared state है — “क्या queue खाली नहीं?”, “क्या flag set है?” — “क्या मैं जागा?” नहीं।
waitहमेशा condition पर while loop के भीतर रखें। हर जागने पर condition जाँचें, और यदि पूरी न हो तो फिर सो जाएँ।- Condition update और check एक ही lock के अधीन करें। Notify करने वाला state update कर फिर notify करता है।
flowchart TB
accTitle: सही wait loop का flow
accDescr: Lock लें और condition जाँचें; यदि पूरी न हो, lock छोड़ें और सो जाएँ; जागने पर lock पुनः लें और condition check पर लौटें। Lock पकड़े आगे केवल तब बढ़ें जब condition पूरी हो
l["Lock लें"] --> c{"Condition पूरी है?"}
c -->|"नहीं"| s["wait(lock छोड़ें और सोएँ)"]
s --> wk["जाग(lock पुनः लें)"]
wk --> c
c -->|"हाँ"| go["Lock पकड़े आगे बढ़ें"]
चित्र 4: सही wait loop है, और condition जाँचने तथा process करने के बीच अंतराल नहीं (दोनों lock पकड़े होते हैं)।
इस रूप का आसानी से छूटने वाला लाभ है। जिस क्षण while loop छोड़ते हैं, lock अभी पकड़े यह स्थापित है कि “condition पूरी है”। Spurious wakeup से बचाने वाला loop, ज्यों का त्यों, गारंटी है कि condition जाँचने और process करने के बीच race-condition अंतराल नहीं।
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
Notification (WakeConditionVariable) lock के भीतर या बाहर से बुला सकते हैं, पर document कहता है कि lock छोड़ने के बाद जगाना आमतौर पर बेहतर है, context switch घटाने के लिए।1 दूसरी ओर, state update स्वयं (++queueCount) हमेशा lock के अधीन होना चाहिए। दोनों न मिलाएँ।
C++ में बुनियादी रूप — predicate wait को 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();
क्योंकि predicate-रूप wait loop आपके लिए करती है, हाथ से लिखा while अनावश्यक है। जब मौजूदा code ठीक करें जिसमें अभी हाथ-लिखा loop हो, while (q.empty()) cv.wait(lk); सही रूप है, इसलिए जल्दी rewrite की ज़रूरत नहीं। एकमात्र गलत रूप if (q.empty()) cv.wait(lk); है।
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 केवल lock (lock block) के भीतर से बुलाए जा सकते हैं, जो Win32 से अलग है। Lock के बाहर बुलाने से SynchronizationLockException फेंकता है।10
Timeout के साथ wait — समय सीमा से शेष समय गणना करें
जब timeout के साथ wait करें, हर loop cycle पर “वही timeout मान” देना हर spurious wakeup पर wait खींचता है। सही रूप है पहले deadline तय करना और शेष समय पुनः गणना करना।
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);
flowchart TB
accTitle: Timeout वाली wait का सही flow
accDescr: पहले deadline तय करें, और हर जागने पर condition और deadline जाँचें; यदि समय बचा हो, शेष समय पुनः गणना कर wait पर लौटें
d["Deadline तय करें"] --> c{"Condition पूरी है?"}
c -->|"हाँ"| go["Processing पर जाएँ"]
c -->|"नहीं"| t{"Deadline बीत गई?"}
t -->|"हाँ"| to["Timeout handle करें"]
t -->|"नहीं"| w["शेष समय गणना कर wait"]
w --> c
चित्र 5: Timeout वाली wait फिर “वही wait अवधि” नहीं देती; deadline से शेष समय पुनः गणना करती है।
C++ में, यह गणना, deadline सहित, wait_until (absolute time) प्लस predicate overload पर छोड़ सकते हैं। Timeout पर लौटने पर भी वह predicate का अंतिम मान देता है, इसलिए “क्या timeout हुआ, या पहुँच गए?” predicate पर भी तय कर सकते हैं।5
6. बचने योग्य patterns की सूची
केवल एक बार if से जाँचना। यही लेख का नायक है। जिस क्षण spurious wakeup या stolen wakeup हो, condition अधूरी रहते processing आगे बढ़ती है। खाली queue से लेना, uninitialized data छूना, double free — symptom बनता है “crash या data corruption जो केवल कभी-कभी दिखता है”।
Lock के बाहर condition जाँचना या update करना। यदि waiter lock के बाहर condition देखे, “अभी नहीं” तय करे, और wait में प्रवेश से पहले के अंतराल में notify करने वाला state update कर notification भेजे, notification बिना waiter वाली condition variable पर दाग दी जाती है और गायब हो जाती है। Waiter तब wait में प्रवेश करता है और ऐसी notification की wait करता रहता है जो फिर कभी नहीं आएगी। वह lost wakeup है, spurious wakeup की दर्पण छवि। Condition variable की wait API के “lock atomically छोड़कर सो जाना” design का कारण ठीक यही अंतराल बंद करना है।1 जब तक lock discipline रखें यह नहीं होता।
sequenceDiagram
accTitle: Lost wakeup की timeline
accDescr: यदि waiter lock के बाहर condition जाँचे और notify करने वाला wait में प्रवेश से पहले के अंतराल में state update कर notify करे, notification बिना waiter वाली condition variable पर जाती और गायब होती है, और waiter ऐसी notification की wait करता रहता है जो कभी नहीं आएगी
participant W as Waiter
participant N as Notifier
W->>W: Lock के बाहर condition जाँचें(अधूरी)
N->>N: State update करें और notify करें
Note over N: इस क्षण कोई waiter नहीं
W->>W: wait में प्रवेश
Note over W: Notification पहले ही गई और कभी नहीं जागता
चित्र 6: यदि lock के बाहर condition जाँचें, notification जाँच और wait के अंतराल से निकल जाती है — “lost wakeup”।
Condition variable की “transient notification” event पर pulse से दोहराना। Event स्वयं (CreateEvent + SetEvent) anti-pattern नहीं। उस setup में जाग संकेत जहाँ एक consumer queue खाली होने तक process करे, या एक बार उठाई कभी न गिराई stop instruction (manual-reset event), event के सही इस्तेमाल हैं; और जब WaitForMultipleObjects से अन्य wait targets से जोड़ना हो, या process सीमा पार करनी हो, condition variable — user-mode object जो process-across share नहीं हो सकता — वह है जो इस्तेमाल नहीं हो सकता।1 खतरनाक है event operation से condition variable की transient notification दोहराने की कोशिश जो “केवल उस क्षण wait कर रहे thread जगाए और state पीछे न छोड़े”। वह विचार लगभग हमेशा अगली वस्तु, PulseEvent, तक ले जाता है।
PulseEvent इस्तेमाल करना। यह API manual-reset event पर “अभी wait कर रहे सबको जगाए और तुरंत event को non-signaled state पर लौटाए” है, पर Microsoft स्वयं document में कहता है कि “यह function unreliable है और इस्तेमाल नहीं होनी चाहिए। यह मुख्यतः backward compatibility के लिए मौजूद है। इसके बजाय condition variable इस्तेमाल करें।” कारण यह है कि wait thread kernel-mode APC से wait state से अस्थायी हटाया जा सकता है और APC पूरा होने के बाद wait पर लौट सकता है। यदि उस संक्षिप्त अंतराल में PulseEvent बुलाया जाए, वह thread “जिनकी उस क्षण wait थी जब बुलाया गया” में शामिल नहीं और नहीं जागता।7 Kernel APC वह हैं जो OS internally इस्तेमाल करता है; ऐप उन्हें control नहीं कर सकता।11 यह समस्या static-analysis warning (C28648) भी है।12 यदि spurious wakeup “extra जगाने” की समस्या है, यह “जब जागना चाहिए था तब सोए रहना” की समस्या है, और while loop नहीं बचा सकता — क्योंकि notification स्वयं खो गई है।
State update से पहले, lock पकड़े बिना, केवल notification पहले भेजना। State अभी stale रहते WakeConditionVariable बुलाना, और तभी lock लेकर state update करना — उस क्रम में, जागा thread जाँचते समय condition अभी अधूरी देखता है, और फिर सो जाता है। यदि आगे notification न आए, वहीं रहता है। ध्यान दें कि यदि उसी lock पकड़े “notify → update → छोड़ें” लिखें, वास्तविक हानि नहीं, क्योंकि waiter condition तब तक नहीं जाँच सकता जब तक lock पुनः न ले। फिर भी, ताकि readers को यह safety condition हर बार verify न करनी पड़े, क्रम “lock के अधीन state update करें, और उसके बाद notify करें” पर standardize करना अधिक सुरक्षित है।
7. जब इसमें मिलें तो investigation कैसे करें
Spurious wakeup वाले bug की विशेषता है “केवल दुर्लभ दिखना”। Symptom से पीछे चलें, तो निम्नलिखित दो परिवारों में बँटते हैं।
परिवार 1: Condition अधूरी रहते processing आगे बढ़ती है। खाली queue से लेने से exception या crash, छूटे results, आदि। Predicate-रहित wait संदेह करें। इसे code review में यंत्रवत छान सकते हैं — उन जगहों की खोज करें जहाँ cv.wait( का केवल एक argument हो, और जहाँ SleepConditionVariableCS / Monitor.Wait while के बजाय if में लिपटे हों। इस जाँच को reproduce की wait नहीं, और यह आपके पास सबसे अधिक लाभ वाला कदम है।
परिवार 2: जो thread जागना चाहिए वह नहीं जागता (hang)। Lost wakeup (lock के बाहर condition जाँचना, या state update से पहले lock के बाहर notify करना) और PulseEvent संदेह करें। Hang process से dump लें और हर thread का stack देखें, तो पहचान सकते हैं कौन सा thread किस wait API में अटका है। वहाँ से code में पीछा करें “वह notification किसे भेजनी थी, और किस क्रम में”।
flowchart TB
accTitle: Symptom से triage flow
accDescr: यदि condition अधूरी रहते processing आगे बढ़े, code खोज से predicate-रहित waits छानें; यदि thread न जागे, dump से wait स्थल पहचानें और lost wakeup या PulseEvent संदेह करें
s["केवल दुर्लभ दिखने वाला bug"] --> a["Condition अधूरी रहते processing आगे"]
s --> b["जो thread जागना चाहिए वह नहीं जागता"]
a --> a1["Predicate-रहित wait के लिए code खोजें"]
b --> b1["Dump से wait thread पहचानें"]
a1 -.-> a2["if को while करें, या predicate wait"]
b1 -.-> b2["Lost wakeup या PulseEvent संदेह"]
चित्र 7: Symptom “बहुत आगे बढ़ना” है या “कभी न जागना” दोनों संदेह और investigation का तरीका बाँटता है।
यदि reproduce करना चाहें, मानक कदम race window चौड़ा करना है। Physical cores से अधिक threads इस्तेमाल कर, wait और notification के बीच जानबूझकर Sleep घुसाकर, और debug तथा release दोनों builds चलाकर time jitter बढ़ाएँ। जब पुष्टि हो कि “predicate-रहित wait ठीक करने के बाद reproduce रुका”, उसी तनाव के अधीन तुलना करें।
8. सारांश — checklist
waitसे वापस तीन paths हैं — वास्तविक notification, spurious wakeup, और stolen wakeup — और caller उन्हें अलग नहीं कर सकता। इसलिए wait हमेशा condition पर while loop लिखें।- Spurious wakeup वह व्यवहार है जो Win32, C++, और POSIX ने performance के विरुद्ध समझौते के रूप में जानबूझकर अनुमति दी, और OS fix या library बदलने से नहीं जाएगा। .NET का
Monitor.Waitबिना कारण जागने वाला नहीं माना जाता, पर क्योंकि stolen wakeup और timeout हैं, वही while discipline फिर भी आवश्यक है। - C++ में, predicate रूप
wait(lock, pred)default करें। Library loop करती है। - Condition update और check एक ही lock के अधीन करें। Notification “state update के बाद” भेजें। Win32/C++ notification lock छोड़ने के बाद हो सकती है; C# का
Pulseकेवल lock के भीतर। - Timeout वाली wait के लिए deadline तय करें और शेष समय पुनः गणना करें। C++ में,
wait_untilप्लस predicate। - Condition variable की transient notification event पर pulse से न दोहराएँ। खासकर
PulseEventवह है जिसके बारे में official document साफ़ शब्दों में कहता है “इस्तेमाल न करें, इसके बजाय condition variable इस्तेमाल करें”। Event स्वयं stop instruction,WaitForMultipleObjectsसे जोड़ना, और process-across synchronization के लिए सही उपकरण रहते हैं। - Review में, “predicate-रहित wait” और “
if+ wait” यंत्रवत खोजें। दुर्लभ-reproduce bug को reproduce की wait किए बिना मार सकते हैं।
Spurious wakeup, नाम की विचित्रता के विपरीत, fix के एक-पंक्ति keyword में संघनित होता है — if को while करें। और उस एक पंक्ति के पीछे condition-variable उपकरण का design विचार है: “सटीक notification महँगी है, इसलिए जाँचना waiter की जिम्मेदारी है”। इसे mechanism के रूप में समझें तो भाषा या framework बदलने पर भी वही discipline बिना हिचकिचाहट लागू कर सकेंगे।
संबंधित लेख
- Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा दुर्घटनाएँ हटाना
- Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
- Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
- Windows पर Sleep(1) के बजाय Event Wait क्यों चुनें
- Shared memory की गलतियाँ और practical best practices
- Windows I/O की गहराई (भाग 2) — synchronous और async I/O: OVERLAPPED का वास्तविक अर्थ
संबंधित consulting क्षेत्र
KomuraSoft LLC multithread design review, “केवल कभी-कभी reproduce” crash और hang की root-cause investigation (dump analysis), तथा legacy synchronization code (event- और PulseEvent-dependent, आदि) को condition-variable आधार पर migrate करना संभालता है। Symptom के triage से शुरू करना ठीक है — बेझिझक संपर्क करें।
संदर्भ लिंक
-
Microsoft Learn, Condition Variables. Condition variable के user-mode object होने पर जो lock atomically छोड़कर wait में प्रवेश करता है; spurious wakeup (explicit wake से न बँधे जाग) और stolen wakeup (जागे thread से पहले दूसरे thread का चलना) होने पर, जिससे wait से लौटने के बाद predicate while loop में फिर जाँचना चाहिए; और notification lock के भीतर या बाहर से संभव होने, पर lock छोड़ने के बाद जगाने के context switch घटाने के लिए बेहतर होने पर। ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, SleepConditionVariableCS function (synchapi.h). Specified critical section atomically छोड़कर condition variable पर wait करने पर; जागे thread के लौटने से पहले critical section पुनः लेने पर; timeout पर ERROR_TIMEOUT लौटने पर; और spurious wakeup तथा stolen wakeup होने पर, जिससे wait से लौटने के बाद predicate (आमतौर पर while loop में) फिर जाँचना चाहिए। ↩ ↩2
-
The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. pthread_cond_wait / pthread_cond_timedwait से spurious wakeup हो सकने पर; wait से लौटने का predicate के मान के बारे में कुछ न कहने पर, इसलिए predicate का re-evaluate करना चाहिए; और Rationale के यह कहने पर कि “ठीक एक जगाओ” वाली implementation condition-variable operations खासकर multiprocessor पर धीमे कर सकती है, और spurious wakeup अनुमति देना predicate-check loop बाध्य कर ऐप अधिक मज़बूत बनाता है। ↩ ↩2 ↩3
-
cppreference.com, std::condition_variable::wait. Predicate-रहित wait के spurious wakeup से unblock हो सकने पर; और predicate overload के while (!pred()) wait(lock); के समकक्ष होने तथा हर notification या spurious wakeup पर lock पुनः लेकर predicate जाँचने वाले loop के रूप में परिभाषित होने पर। ↩ ↩2
-
Microsoft Learn, condition_variable Class. Predicate-रहित wait के notify_one / notify_all पर unblock होने और spurious भी जाग सकने पर; predicate रूप wait(lock, pred) के effectively while (!Pred()) wait(Lck); चलाने पर; और wait_for / wait_until के वही गुण तथा predicate overload रखने पर। ↩ ↩2 ↩3
-
Microsoft Learn, Monitor.Wait Method. Wait के lock छोड़कर wait queue में प्रवेश करने पर; Pulse / PulseAll से जागने के बाद lock पुनः लेने तक न लौटने पर; और अभिप्रेत इस्तेमाल के जागे thread के उस condition का re-evaluate करने तथा यदि आवश्यक हो तो Wait फिर बुलाने पर जिसके कारण wait में आया। ↩ ↩2
-
Microsoft Learn, PulseEvent function (winbase.h). Wait thread के kernel-mode APC से wait state से अस्थायी हटाए जा सकने और APC पूरा होने के बाद लौटने पर, जिससे उस अंतराल में PulseEvent बुलाए जाने पर thread मुक्त न होने पर; और PulseEvent के इसलिए unreliable तथा नए ऐप में इस्तेमाल न करने, इसके बजाय condition variable इस्तेमाल करने पर। ↩ ↩2
-
Microsoft Learn, Using Condition Variables. एक critical section और दो condition variables (BufferNotEmpty और BufferNotFull) से producer–consumer queue लागू करने वाले official sample पर। Wait predicate जाँचने वाले loop के भीतर की जाती है। ↩
-
Microsoft Learn, WaitOnAddress function (synchapi.h). Address का मान बदलने की wait करने वाली function के signaled होने पर लौटना guaranteed पर अन्य कारणों से लौटना भी अनुमति होने पर; early wake के उदाहरणों में low-memory स्थिति, उसी address के लिए पिछला जाग त्यागना, और checked build चलाना शामिल होने पर; और इसलिए लौटने के बाद मान फिर compare करने की आवश्यकता पर, official sample स्वयं while loop होने पर। ↩
-
Microsoft Learn, Monitor.PulseAll Method. PulseAll के threads wait queue से ready queue पर ले जाने, और lock छूटने पर ready queue के अगले thread के lock लेने पर; और Pulse / PulseAll / Wait के केवल synchronization block के भीतर से बुलाने योग्य होने पर। ↩ ↩2
-
Microsoft Learn, Waits and APCs. Kernel APC के preemptive execution पर, और सिस्टम के internally wait API से लौटे बिना wait interrupt और फिर शुरू करने पर, जिससे KePulseEvent जैसा transient signal उस अंतराल में छूट सकता है। ↩
-
Microsoft Learn, C28648: PulseEvent is an unreliable function. PulseEvent इस्तेमाल पर static analysis warning पर; APC के कारण wait से बाहर thread के मुक्त न होने और हमेशा hang रह सकने पर; और SetEvent या अन्य synchronization object से बदलने के मार्गदर्शन पर। ↩
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
DllMain और Loader Lock — DLL initialization में "कुछ मत करो" कहा जाने का असली कारण
DllMain से LoadLibrary क्यों न call करें और दूसरे thread से synchronize न करें। Primary sources से यह लेख बताता है कि loader lock हर DLL ...
Win32 Thread Pool API — CreateThreadpoolWork से thread बनाए बिना concurrency
क्या आपके native code में CreateThread calls बिखरी पड़ी हैं? यह लेख Vista में redesign की गई Win32 thread pool API — work, timer, wait और...
Named Pipes व्यवहार में — design से security तक, Windows की standard IPC
Named pipes — Windows की standard inter-process communication — की practical guide। यह लेख primary sources से byte और message mode का चुन...
"Not Responding" वास्तव में क्या है — Windows ऐप के hang का फैसला कैसे करता है, और hang न करने वाले ऐप कैसे design करें
Windows का "Not Responding" वह mechanism है जिसमें OS तय करता है कि window ने 5 सेकंड message नहीं लिया और उसे ghost window से बदल देता ह...
Sleep से resume पर टूटने वाले apps — Windows power events कैसे काम करते हैं और उनसे बचने वाले business apps कैसे बनाएँ
Laptop खोला और business app के connections मर चुके थे — कारण वह design है जिसने sleep का हिसाब ही नहीं रखा। यह लेख WM_POWERBROADCAST noti...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
इस विषय से जुड़ी सेवाएँ
यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।
Windows ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- क्या spurious wakeup OS या library का bug है?
- नहीं — यह spec में लिखा व्यवहार है। Win32 की SleepConditionVariableCS, C++ की std::condition_variable, और POSIX की pthread_cond_wait सबके official documents या standard साफ़ कहते हैं कि notification से न बँधा जागना हो सकता है। उसे मना करने वाली implementation theoretically संभव है, पर वह हर condition-variable operation धीमा करेगी (खासकर multiprocessor पर notification), इसलिए समझौता है इसे अनुमति देना इस समझ पर कि "यदि waiter condition फिर जाँचे तो correctness सुरक्षित रहती है"। इसलिए उपाय OS fix की wait नहीं, wait को हमेशा while loop में लिखना है (या predicate-रूप wait इस्तेमाल करना)।
- क्या wait को while loop में लपेटने से performance घटता है?
- व्यवहार में cost नगण्य है। While loop केवल हर जागने पर एक extra condition check जोड़ता है, और वह सस्ती comparison है जब आप पहले से lock पकड़े हैं। Spurious wakeup स्वयं दुर्लभ हैं, इसलिए extra loop cycle केवल exceptional स्थितियों में होता है। Check को if छोड़ने की cost, दूसरी ओर, वह "bug जो केवल दुर्लभ reproduce होता है" है जिसमें condition अधूरी रहते processing आगे बढ़ती है — तुलना नहीं। Condition-variable wait cost पर वास्तव में हावी lock contention और कितनी बार notify करते हैं है, while है या नहीं नहीं।
- यदि C++ की predicate-रूप wait इस्तेमाल करूँ, क्या spurious wakeup भूल सकता हूँ?
- Wait loop के लिए, हाँ: cv.wait(lock, pred) effectively while (!pred()) wait(lock); है, इसलिए spurious wakeup और stolen wakeup दोनों अपने आप सोख लिए जाते हैं। नए C++ code को predicate overload default मानना चाहिए। आपको फिर भी predicate द्वारा पढ़ी shared state के updates उसी mutex से बचाने पड़ते हैं, और notify करने वाले को notify बुलाने से पहले वह state update करनी पड़ती है। Predicate wait loop आपके हाथ से लेती है; lock discipline नहीं।
- क्या वही समस्या C# के Monitor.Wait से होती है?
- हाँ। Monitor.Wait में wait कर रहा thread Pulse/PulseAll से जागता है और फिर Wait से लौटने से पहले lock पुनः लेता है, पर उस अंतराल में दूसरा thread पहले lock ले condition खा सकता है (stolen wakeup)। Microsoft का document इस धारणा पर लिखा है कि जागा thread उस condition का re-evaluate करता है जिसके कारण wait की, और यदि आवश्यक हो तो Wait फिर बुलाता है। इसलिए C# में भी बुनियादी रूप while (!condition) Monitor.Wait(gate); है। Win32 से अलग एक बाधा है कि Wait/Pulse केवल lock statement के भीतर से बुला सकते हैं।
- क्या WaitForSingleObject से event की wait पर भी spurious wakeup होते हैं?
- साधारण (non-alertable) wait में, WAIT_OBJECT_0 केवल तब लौटता है जब object वास्तव में signaled हो; condition variable जैसी "बिना कारण जाग" नहीं होती। इतना कहकर, "event signaled हुआ" और "आपके ऐप की condition पूरी है" अलग बातें हैं। यदि कई consumers उसी event से जागें, पहले lock लेने वाला thread condition खाता है, इसलिए जागने के बाद condition फिर जाँचनी पड़ती है। जो design condition variable की "उस क्षण wait कर रहे को ही जगाओ" transient notification event से दोहराने की कोशिश करते हैं वे PulseEvent की reliability समस्या में भी फँसते हैं, इसलिए process के भीतर condition की wait के लिए condition variable अधिक सुरक्षित उपकरण है।