स्प्यूरियस वेकअप — Condition Variable "बिना सूचना के" क्यों जागती है और Windows पर सही प्रतीक्षा कैसे करें

· · Windows, मल्टीथ्रेडिंग, कंडीशन वेरिएबल, सिंक्रोनाइज़ेशन, C++, C#, Win32 API, समस्या निवारण

“हम कतार में डेटा रखते हैं और प्रतीक्षा कर रहे वर्कर थ्रेड को जगाते हैं। छह महीने चल रहा था, फिर एक दिन खाली कतार पढ़ने की कोशिश कर क्रैश हो गया।” “हम सूचना भेज रहे हैं, पर कभी-कभी थ्रेड जागता ही नहीं।” — मल्टीथ्रेडेड रेंडेज़वू ऐसा लगता है मानो काम कर रहा हो, और दुर्लभ दिखने वाले बग का प्रजनन स्थल है। इस तरह की जाँचें अक्सर उस कोड पर उतरती हैं जो कंडीशन वेरिएबल की wait को if में लपेटता है। और उसके पीछे बैठता है स्प्यूरियस वेकअप — सूचना पाए बिना wait से लौटने की घटना।

“कोई सूचित किए बिना जागता है” इम्प्लीमेंटेशन का दोष लगता है, पर यह व्यवहार Win32, C++, और POSIX सबके दस्तावेज़ या मानकों में लिखा है, और .NET का Monitor इस धारणा पर डिज़ाइन है कि “एक बार जागने पर, शर्त फिर जाँचें”। वह व्यवहार अनुमति क्यों है? Windows पर वह किस परत में होता है? और प्रतीक्षा कैसे लिखें कि कभी न लगें? Windows पर व्यावसायिक ऐप और उपकरण-नियंत्रण सॉफ़्टवेयर लिखने वाले डेवलपरों के लिए, यह लेख प्राथमिक स्रोतों से स्प्यूरियस वेकअप वास्तव में क्या है खोलता है, और सही प्रतीक्षा को Win32 (C), C++, तथा C# में उबालता है।

1. निष्कर्ष पहले

  • कंडीशन वेरिएबल की wait सूचना आए बिना भी लौट सकती है। Win32 का आधिकारिक दस्तावेज़ कहता है कि कंडीशन वेरिएबल स्प्यूरियस वेकअप (स्पष्ट जाग से न बँधे जाग) और चुराए वेकअप (जागे थ्रेड से पहले दूसरे थ्रेड का शर्त खाना) के अधीन हैं।1
  • इसलिए प्रतीक्षा हमेशा “while लूप प्लस शर्त की पुनः-जाँच” लिखनी चाहिए। जो कोड एक बार if से जाँच कर फिर wait करता है ऐसा लगता है मानो काम कर रहा हो, और दुर्लभ पुनरुत्पादित बग पालता है।12
  • यह Windows-विशिष्ट विचित्रता नहीं; POSIX और C++ मानक वही कहते हैं। “बिल्कुल कभी स्प्यूरियस न जागे” वाली इम्प्लीमेंटेशन हर कंडीशन-वेरिएबल ऑपरेशन धीमा करेगी, इसलिए जाग इस धारणा पर अनुमति है कि प्रतीक्षाकर्ता फिर जाँचेगा।34
  • C++ में, प्रेडिकेट रूप wait(lock, pred) लाइब्रेरी से लूप करवाता है। वह रूप प्रभावी रूप से while (!pred()) wait(lock); चलाता है। नए कोड के लिए यही डिफ़ॉल्ट है।5
  • C# के Monitor.Wait को वही अनुशासन चाहिए। जागने और लॉक पुनः लेने के अंतराल में शर्त खाई जा सकती है, इसलिए शर्त while में फिर जाँचें और Wait पर लौटें।6
  • शर्त अपडेट और जाँच एक ही लॉक के अधीन करें। यदि लॉक के बाहर शर्त देखकर wait में प्रवेश करें, सूचना अंतराल से निकल सकती है — खोया वेकअप।1
  • कंडीशन वेरिएबल की “अभी प्रतीक्षा कर रहे को जगाओ” क्षणिक सूचना इवेंट पर पल्स से न दोहराएँ। विशेषकर PulseEvent उस क्षण सूचना चूक सकता है जब कर्नेल-मोड APC प्रतीक्षा संक्षेप में उठाए, और Microsoft स्वयं साफ़ शब्दों में कहता है, “यह अविश्वसनीय है, उपयोग न करें, इसके बजाय कंडीशन वेरिएबल उपयोग करें”।7

आगे इस निष्कर्ष को सहारा देने वाले तंत्र क्रम से चलते हैं।

2. स्प्यूरियस वेकअप क्या है — जागना शर्त पूरी होने का अर्थ नहीं

कंडीशन वेरिएबल “थ्रेड को तब तक सुलाने का सिंक्रोनाइज़ेशन प्रिमिटिव है जब तक कोई शर्त पूरी न हो, और होने पर जगाने” के लिए है। Win32 पर वह CONDITION_VARIABLE संरचना के साथ SleepConditionVariableCS / SleepConditionVariableSRW (प्रतीक्षा) और WakeConditionVariable / WakeAllConditionVariable (सूचना) है। प्रतीक्षा API आपके पकड़े लॉक (क्रिटिकल सेक्शन या SRW लॉक) को परमाणु रूप से छोड़ती और सो जाती है, और जागने पर लौटने से पहले लॉक पुनः लेती है।1

प्रश्न है “wait से लौटना” वास्तव में क्या अर्थ रखता है। भोलेपन से सोचना चाहते हैं “सूचना आई = शर्त पूरी है”, पर वास्तव में तीन स्थितियाँ हैं जिनमें wait लौटती है।

स्थिति सूचना लौटने पर शर्त
वास्तविक जाग हाँ अक्सर पूरी, पर गारंटी नहीं
स्प्यूरियस वेकअप आपके नाम कोई नहीं अभी अधूरी
चुराया वेकअप हाँ दूसरे थ्रेड ने पहले खा ली; अधूरी
वे तीन स्थितियाँ जिनमें wait लौटती हैकंडीशन वेरिएबल प्रतीक्षा न केवल वास्तविक सूचना से लौट सकती है बल्कि बिना सूचना वाले स्प्यूरियस वेकअप से और उस चुराए वेकअप से भी जहाँ सूचना आई पर शर्त पहले खा ली गई, इसलिए हर स्थिति में शर्त फिर जाँचनी पड़ती हैwait से लौटेवास्तविक सूचनास्प्यूरियस वेकअप(कोई सूचना नहीं)चुराया वेकअप(शर्त पहले खा ली)शर्त फिर जाँचें, फिर आगे बढ़ें

चित्र 1: wait से वापस तीन पथ हैं, और कॉलर नहीं बता सकता कौन सा लिया, इसलिए शर्त हमेशा फिर जाँचनी चाहिए।

स्प्यूरियस वेकअप यह दूसरी स्थिति है — प्रतीक्षा API के आपको जगाने के लिए लिखी स्पष्ट सूचना से न बँधे लौटने की घटना। यह उन स्थितियों तक सीमित नहीं जिनमें सिस्टम में कहीं WakeConditionVariable कभी न बुलाया गया हो। उदाहरण के लिए, उच्च लोड पर जहाँ सूचनाएँ छोटी फुहार में आएँ, इम्प्लीमेंटेशन अतिरिक्त प्रतीक्षा थ्रेड बैच में जगा सकती है, और जिस पक्ष के पास संगत सूचना नहीं उसके लिए वह भी स्प्यूरियस वेकअप है। Microsoft Learn का कंडीशन-वेरिएबल पृष्ठ यह साफ़ कहता है: “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

महत्त्वपूर्ण बिंदु यह है कि कॉलर नहीं बता सकता तीन में से किस स्थिति से लौटा। यदि नहीं बता सकते, तो केवल एक रणनीति उपलब्ध है: हर बार लौटने पर, जिस शर्त की प्रतीक्षा कर रहे थे उसे स्वयं जाँचें, और यदि पूरी न हो तो फिर सो जाएँ। यही लौह नियम “wait को while में लपेटें” की वास्तविक सामग्री है। उलटा कहें, जब तक वह नियम रखें, तीन में से कोई भी स्थिति जगाए कोड सही है।

3. विनिर्देश इसे क्यों अनुमति देता है — सटीक सूचना महँगी है

“बिना सूचना जागना तो ढीली इम्प्लीमेंटेशन है, है न?” उचित प्रश्न है। वास्तव में कभी स्प्यूरियस न जागने वाली इम्प्लीमेंटेशन बनाना सैद्धांतिक रूप से संभव है। फिर भी, POSIX, Windows, और C++ मानक सब “हो सकता है” पक्ष पर आए। कारण POSIX (The Open Group Base Specifications) में pthread_cond_wait के Rationale में खुलकर लिखा है।3

पहला कारण प्रदर्शन है। “विश्वसनीय रूप से ठीक एक थ्रेड जगाओ” सूचना सख्ती से लागू करने की कोशिश, विशेषकर मल्टीप्रोसेसर पर, हर कंडीशन-वेरिएबल ऑपरेशन पर अतिरिक्त सिंक्रोनाइज़ेशन लागत जोड़ती है। सूचना और जागने के बीच शेड्यूलर बैठता है, और व्यवधान तथा प्रीएम्प्शन के समय पर आप “जिसे जगाना था उससे पहले दूसरा थ्रेड चलना” नहीं बचा सकते। उसे पूरी तरह सील करने की लागत सबको चुकाना, कंडीशन वेरिएबल तेज़ रखने के लिए, इससे बदतर है कि स्वीकार करें “कभी-कभी अतिरिक्त जगा सकते हैं”।

दूसरा कारण यह अवलोकन है कि यह समझौता ऐप नहीं तोड़ता — वास्तव में उन्हें अधिक मज़बूत बनाता है। क्योंकि स्प्यूरियस वेकअप अनुमति हैं, सही कोड हमेशा प्रेडिकेट (जिस शर्त की प्रतीक्षा है) जाँचने वाला लूप लिखता है। POSIX का Rationale कहता है कि इस लूप को बाध्य करना कोड स्व-दस्तावेज़ी और अधिक मज़बूत बनाता है।3 एक बार लूप हो, सूचना का अर्थ “शर्त पूरी होने की गारंटी” से गिरकर “संकेत कि शर्त बदल गई हो सकती है” हो जाता है, और प्रतीक्षा पक्ष सूचित करने वाले के मामूली डिज़ाइन परिवर्तन (बहुत जगाना, बैच में जगाना, आदि) सहने लगता है।

चुराए वेकअप और भी अधिक संरचनात्मक बात हैं। सूचित करने वाले के WakeConditionVariable बुलाने और जागे थ्रेड के लॉक पुनः लेकर wait से लौटने के बीच हमेशा समय अंतराल होता है। यदि तीसरा थ्रेड उस अंतराल में लॉक ले सके, वह शर्त (कतार की सामग्री, आदि) पहले खा सकता है। वह अंतराल कोई भी इम्प्लीमेंटेशन चमकाने से मिट नहीं सकता, क्योंकि वह कंडीशन-वेरिएबल उपकरण के रूप से आता है।

चुराए वेकअप की टाइमलाइनउत्पादक कतार में एक आइटम रखता और प्रतीक्षा कर रहे उपभोक्ता A को जगाता है, पर A लॉक पुनः लेने से पहले उपभोक्ता B लॉक ले एक आइटम ले लेता है, इसलिए A के जागने तक कतार खाली हैउपभोक्ता Bउत्पादकउपभोक्ता A(प्रतीक्षा)उपभोक्ता Bउत्पादकउपभोक्ता A(प्रतीक्षा)जागा, लॉक पुनः लेने की प्रतीक्षाकतार खाली(चुराया)कतार में एक आइटम जोड़ेंWakeConditionVariableलॉक लें और एक आइटम लेंलॉक पुनः लें और wait से लौटेंwhile लूप में फिर जाँचें और फिर प्रतीक्षा

चित्र 2: “चुराया वेकअप”, जिसमें तीसरा थ्रेड सूचना और जागने के समय अंतराल में शर्त खाता है, किसी भी इम्प्लीमेंटेशन के अधीन हो सकता है।

अर्थात्, भले OS स्प्यूरियस वेकअप पूरी तरह मिटा दे, जब तक चुराए वेकअप हैं आप फिर भी “मैं जागा = शर्त पूरी है” नहीं लिख सकते। प्रतीक्षाकर्ता का पुनः-जाँच लूप वैसे भी चाहिए, और उसे देखते हुए, स्प्यूरियस वेकअप अनुमति देकर इम्प्लीमेंटेशन तेज़ रखना सस्ता है — यही डिज़ाइन निर्णय कंडीशन वेरिएबल दशकों से ढोती हैं।

4. Windows पर यह किन परतों में दिखता है

यह गुण Windows सिंक्रोनाइज़ेशन प्रिमिटिव की जिस परत का API लिखें उस पर अपना चेहरा दिखाता है। इस तथ्य का अहसास पाने के लिए कि किसी भी परत से नहीं बच सकते, प्रतिनिधि परतें देखेंगे।

Win32 कंडीशन वेरिएबल (CONDITION_VARIABLE) पहले ही नोट किए अनुसार SleepConditionVariableCS / SleepConditionVariableSRW पर स्प्यूरियस वेकअप और चुराए वेकअप दोनों के अधीन दस्तावेज़ीकृत हैं, और प्रेडिकेट while लूप में फिर जाँचनी आवश्यक है।2 आधिकारिक उपयोग नमूना (उत्पादक–उपभोक्ता कतार) भी प्रतीक्षा while लूप के भीतर लिखता है।8

और निचली WaitOnAddress कंडीशन वेरिएबल से अधिक आदिम प्रतीक्षा API है: “दिए पते पर मान बदलने तक प्रतीक्षा” (Windows 8 और बाद)। यहाँ तक यह लगभग-नीचे की परत API दस्तावेज़ कहती है “पता signaled होने पर लौटना गारंटीशुदा है, पर अन्य कारणों से लौटना भी अनुमति है”, और जल्दी जागने के उदाहरणों में कम-मेमोरी स्थिति, उसी पते के लिए पिछला जाग त्यागना, और checked बिल्ड चलाना गिनती है। इसीलिए दस्तावेज़ का अपना उपयोग नमूना “मान फिर तुलना करने वाले while लूप” के रूप में है।9

C++ की std::condition_variable वही है। MSVC का दस्तावेज़ प्रेडिकेट-रहित wait के बारे में कहता है कि वह “notify_one / notify_all कॉल से signaled होने तक ब्लॉक करती है। वह स्प्यूरियस भी जाग सकती है”, और समझाता है कि प्रेडिकेट रूप wait(lock, pred) प्रभावी रूप से निम्नलिखित कोड चलाता है।5

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

अर्थात्, C++ में अनुशंसित प्रेडिकेट-रूप wait और कुछ नहीं बल्कि लाइब्रेरी का “while में लपेटें”, जैसा यह लेख वर्णन करता है, आपके हाथ से लेना है। cppreference भी कहती है कि प्रेडिकेट-रहित wait स्प्यूरियस अनब्लॉक हो सकती है।4

.NET का Monitor.Wait / Pulse अपनी प्रतीक्षा कतार और तैयार कतार की कतार संरचना रखता है, पर अनुशासन नहीं बदलता। Pulse / PulseAll से जागा थ्रेड तैयार कतार पर जाता है और लॉक पुनः ले सकने के क्रम में Wait से लौटता है। लॉक पुनः लेने से पहले दूसरे थ्रेड का शर्त खा सकना Win32 जैसा ही है, और दस्तावेज़ भी इस धारणा पर लिखा है कि “जागा थ्रेड उस शर्त का पुनर्मूल्यांकन करता है जिसके कारण प्रतीक्षा में आया, और यदि आवश्यक हो तो Wait फिर बुलाता है”।610

हर परत प्रेडिकेट फिर जाँचने की माँग करती हैआधिकारिक दस्तावेज़ हर परत पर जागने के बाद शर्त फिर जाँचने की माँग करता है — C++ std::condition_variable, .NET Monitor, Win32 CONDITION_VARIABLE, और निम्न-स्तरीय WaitOnAddressC++ std::condition_variableजागने पर शर्त फिर जाँचें(while).NET Monitor.WaitWin32 CONDITION_VARIABLEWaitOnAddress

चित्र 3: भाषा या फ़्रेमवर्क बदलें और आधिकारिक आवश्यकता हर प्रतीक्षा-प्रिमिटिव परत पर वही है: जागने के बाद फिर जाँचें।

5. सही प्रतीक्षा — while और प्रेडिकेट से लिखें

यहाँ से, इम्प्लीमेंटेशन। केवल तीन सिद्धांत हैं।

  1. जिसकी प्रतीक्षा करते हैं उसे “सूचना” नहीं, अवस्था (प्रेडिकेट) के रूप में रखें। शर्त लॉक से सुरक्षित साझा अवस्था है — “क्या कतार खाली नहीं?”, “क्या फ़्लैग सेट है?” — “क्या मैं जागा?” नहीं।
  2. wait हमेशा शर्त पर while लूप के भीतर रखें। हर जागने पर शर्त जाँचें, और यदि पूरी न हो तो फिर सो जाएँ।
  3. शर्त अपडेट और जाँच एक ही लॉक के अधीन करें। सूचित करने वाला अवस्था अपडेट कर फिर सूचित करता है।
सही प्रतीक्षा लूप का प्रवाहलॉक लें और शर्त जाँचें; यदि पूरी न हो, लॉक छोड़ें और सो जाएँ; जागने पर लॉक पुनः लें और शर्त जाँच पर लौटें। लॉक पकड़े आगे केवल तब बढ़ें जब शर्त पूरी होनहींहाँलॉक लेंशर्त पूरी है?wait(लॉक छोड़ें और सोएँ)जाग(लॉक पुनः लें)लॉक पकड़े आगे बढ़ें

चित्र 4: सही प्रतीक्षा लूप है, और शर्त जाँचने तथा संसाधित करने के बीच अंतराल नहीं (दोनों लॉक पकड़े होते हैं)।

इस रूप का आसानी से छूटने वाला लाभ है। जिस क्षण while लूप छोड़ते हैं, लॉक अभी पकड़े यह स्थापित है कि “शर्त पूरी है”। स्प्यूरियस वेकअप से बचाने वाला लूप, ज्यों का त्यों, गारंटी है कि शर्त जाँचने और संसाधित करने के बीच रेस-कंडीशन अंतराल नहीं।

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

सूचना (WakeConditionVariable) लॉक के भीतर या बाहर से बुला सकते हैं, पर दस्तावेज़ कहता है कि लॉक छोड़ने के बाद जगाना आमतौर पर बेहतर है, कॉन्टेक्स्ट स्विच घटाने के लिए।1 दूसरी ओर, अवस्था अपडेट स्वयं (++queueCount) हमेशा लॉक के अधीन होना चाहिए। दोनों न मिलाएँ।

C++ में बुनियादी रूप — प्रेडिकेट wait को डिफ़ॉल्ट बनाएँ

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();

क्योंकि प्रेडिकेट-रूप wait लूप आपके लिए करती है, हाथ से लिखा while अनावश्यक है। जब मौजूदा कोड ठीक करें जिसमें अभी हाथ-लिखा लूप हो, while (q.empty()) cv.wait(lk); सही रूप है, इसलिए जल्दी पुनर्लेखन की ज़रूरत नहीं। एकमात्र गलत रूप 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 ब्लॉक) के भीतर से बुलाए जा सकते हैं, जो Win32 से भिन्न है। लॉक के बाहर बुलाने से SynchronizationLockException फेंकता है।10

टाइमआउट के साथ प्रतीक्षा — समय सीमा से शेष समय गणना करें

जब टाइमआउट के साथ प्रतीक्षा करें, हर लूप चक्र पर “वही टाइमआउट मान” देना हर स्प्यूरियस वेकअप पर प्रतीक्षा खींचता है। सही रूप है पहले समय सीमा तय करना और शेष समय पुनः गणना करना।

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);
टाइमआउट वाली प्रतीक्षा का सही प्रवाहपहले समय सीमा तय करें, और हर जागने पर शर्त और समय सीमा जाँचें; यदि समय बचा हो, शेष समय पुनः गणना कर प्रतीक्षा पर लौटेंहाँनहींहाँनहींसमय सीमा तय करेंशर्त पूरी है?प्रोसेसिंग पर जाएँसमय सीमा बीत गई?टाइमआउट सँभालेंशेष समय गणना कर प्रतीक्षा

चित्र 5: टाइमआउट वाली प्रतीक्षा फिर “वही प्रतीक्षा अवधि” नहीं देती; समय सीमा से शेष समय पुनः गणना करती है।

C++ में, यह गणना, समय सीमा सहित, wait_until (निरपेक्ष समय) प्लस प्रेडिकेट ओवरलोड पर छोड़ सकते हैं। टाइमआउट पर लौटने पर भी वह प्रेडिकेट का अंतिम मान देता है, इसलिए “क्या टाइमआउट हुआ, या पहुँच गए?” प्रेडिकेट पर भी तय कर सकते हैं।5

6. बचने योग्य पैटर्न की सूची

केवल एक बार if से जाँचना। यही लेख का नायक है। जिस क्षण स्प्यूरियस वेकअप या चुराया वेकअप हो, शर्त अधूरी रहते प्रोसेसिंग आगे बढ़ती है। खाली कतार से लेना, अनारंभीकृत डेटा छूना, डबल फ्री — लक्षण बनता है “क्रैश या डेटा दूषण जो केवल कभी-कभी दिखता है”।

लॉक के बाहर शर्त जाँचना या अपडेट करना। यदि प्रतीक्षाकर्ता लॉक के बाहर शर्त देखे, “अभी नहीं” तय करे, और wait में प्रवेश से पहले के अंतराल में सूचित करने वाला अवस्था अपडेट कर सूचना भेजे, सूचना बिना प्रतीक्षाकर्ता वाली कंडीशन वेरिएबल पर दाग दी जाती है और गायब हो जाती है। प्रतीक्षाकर्ता तब wait में प्रवेश करता है और ऐसी सूचना की प्रतीक्षा करता रहता है जो फिर कभी नहीं आएगी। वह खोया वेकअप है, स्प्यूरियस वेकअप की दर्पण छवि। कंडीशन वेरिएबल की प्रतीक्षा API के “लॉक परमाणु रूप से छोड़कर सो जाना” डिज़ाइन का कारण ठीक यही अंतराल बंद करना है।1 जब तक लॉक अनुशासन रखें यह नहीं होता।

खोए वेकअप की टाइमलाइनयदि प्रतीक्षाकर्ता लॉक के बाहर शर्त जाँचे और सूचित करने वाला wait में प्रवेश से पहले के अंतराल में अवस्था अपडेट कर सूचित करे, सूचना बिना प्रतीक्षाकर्ता वाली कंडीशन वेरिएबल पर जाती और गायब होती है, और प्रतीक्षाकर्ता ऐसी सूचना की प्रतीक्षा करता रहता है जो कभी नहीं आएगीसूचित करने वालाप्रतीक्षाकर्तासूचित करने वालाप्रतीक्षाकर्ताइस क्षण कोई प्रतीक्षाकर्ता नहींसूचना पहले ही गई और कभी नहीं जागतालॉक के बाहर शर्त जाँचें(अधूरी)अवस्था अपडेट करें और सूचित करेंwait में प्रवेश

चित्र 6: यदि लॉक के बाहर शर्त जाँचें, सूचना जाँच और wait के अंतराल से निकल जाती है — “खोया वेकअप”।

कंडीशन वेरिएबल की “क्षणिक सूचना” इवेंट पर पल्स से दोहराना। इवेंट स्वयं (CreateEvent + SetEvent) एंटी-पैटर्न नहीं। उस सेटअप में जाग संकेत जहाँ एक उपभोक्ता कतार खाली होने तक संसाधित करे, या एक बार उठाई कभी न गिराई रोक निर्देश (manual-reset इवेंट), इवेंट के सही उपयोग हैं; और जब WaitForMultipleObjects से अन्य प्रतीक्षा लक्ष्यों से जोड़ना हो, या प्रोसेस सीमा पार करनी हो, कंडीशन वेरिएबल — यूज़र-मोड ऑब्जेक्ट जो प्रोसेस पार साझा नहीं हो सकता — वह है जो उपयोग नहीं हो सकता।1 खतरनाक है इवेंट ऑपरेशन से कंडीशन वेरिएबल की क्षणिक सूचना दोहराने की कोशिश जो “केवल उस क्षण प्रतीक्षा कर रहे थ्रेड जगाए और अवस्था पीछे न छोड़े”। वह विचार लगभग हमेशा अगली वस्तु, PulseEvent, तक ले जाता है।

PulseEvent उपयोग करना। यह API manual-reset इवेंट पर “अभी प्रतीक्षा कर रहे सबको जगाए और तुरंत इवेंट को non-signaled अवस्था पर लौटाए” है, पर Microsoft स्वयं दस्तावेज़ में कहता है कि “यह फ़ंक्शन अविश्वसनीय है और उपयोग नहीं होनी चाहिए। यह मुख्यतः पिछड़ी संगतता के लिए मौजूद है। इसके बजाय कंडीशन वेरिएबल उपयोग करें।” कारण यह है कि प्रतीक्षा थ्रेड कर्नेल-मोड APC से प्रतीक्षा अवस्था से अस्थायी हटाया जा सकता है और APC पूरा होने के बाद प्रतीक्षा पर लौट सकता है। यदि उस संक्षिप्त अंतराल में PulseEvent बुलाया जाए, वह थ्रेड “जिनकी उस क्षण प्रतीक्षा थी जब बुलाया गया” में शामिल नहीं और नहीं जागता।7 कर्नेल APC वह हैं जो OS आंतरिक उपयोग करता है; ऐप उन्हें नियंत्रित नहीं कर सकता।11 यह समस्या स्टैटिक-विश्लेषण चेतावनी (C28648) भी है।12 यदि स्प्यूरियस वेकअप “अतिरिक्त जगाने” की समस्या है, यह “जब जागना चाहिए था तब सोए रहना” की समस्या है, और while लूप नहीं बचा सकता — क्योंकि सूचना स्वयं खो गई है।

अवस्था अपडेट से पहले, लॉक पकड़े बिना, केवल सूचना पहले भेजना। अवस्था अभी बासी रहते WakeConditionVariable बुलाना, और तभी लॉक लेकर अवस्था अपडेट करना — उस क्रम में, जागा थ्रेड जाँचते समय शर्त अभी अधूरी देखता है, और फिर सो जाता है। यदि आगे सूचना न आए, वहीं रहता है। ध्यान दें कि यदि उसी लॉक पकड़े “सूचित → अपडेट → छोड़ें” लिखें, वास्तविक हानि नहीं, क्योंकि प्रतीक्षाकर्ता शर्त तब तक नहीं जाँच सकता जब तक लॉक पुनः न ले। फिर भी, ताकि पाठकों को यह सुरक्षा शर्त हर बार सत्यापित न करनी पड़े, क्रम “लॉक के अधीन अवस्था अपडेट करें, और उसके बाद सूचित करें” पर मानकीकृत करना अधिक सुरक्षित है।

7. जब इसमें मिलें तो जाँच कैसे करें

स्प्यूरियस वेकअप वाले बग की विशेषता है “केवल दुर्लभ दिखना”। लक्षण से पीछे चलें, तो निम्नलिखित दो परिवारों में बँटते हैं।

परिवार 1: शर्त अधूरी रहते प्रोसेसिंग आगे बढ़ती है। खाली कतार से लेने से अपवाद या क्रैश, छूटे परिणाम, आदि। प्रेडिकेट-रहित प्रतीक्षा संदेह करें। इसे कोड समीक्षा में यंत्रवत छान सकते हैं — उन जगहों की खोज करें जहाँ cv.wait( का केवल एक तर्क हो, और जहाँ SleepConditionVariableCS / Monitor.Wait while के बजाय if में लिपटे हों। इस जाँच को पुनरुत्पादन की प्रतीक्षा नहीं, और यह आपके पास सबसे अधिक लाभ वाला कदम है।

परिवार 2: जो थ्रेड जागना चाहिए वह नहीं जागता (हैंग)। खोया वेकअप (लॉक के बाहर शर्त जाँचना, या अवस्था अपडेट से पहले लॉक के बाहर सूचित करना) और PulseEvent संदेह करें। हैंग प्रोसेस से डंप लें और हर थ्रेड का स्टैक देखें, तो पहचान सकते हैं कौन सा थ्रेड किस प्रतीक्षा API में अटका है। वहाँ से कोड में पीछा करें “वह सूचना किसे भेजनी थी, और किस क्रम में”।

लक्षण से ट्रायज प्रवाहयदि शर्त अधूरी रहते प्रोसेसिंग आगे बढ़े, कोड खोज से प्रेडिकेट-रहित प्रतीक्षाएँ छानें; यदि थ्रेड न जागे, डंप से प्रतीक्षा स्थल पहचानें और खोया वेकअप या PulseEvent संदेह करेंकेवल दुर्लभ दिखने वाला बगशर्त अधूरी रहते प्रोसेसिंग आगेजो थ्रेड जागना चाहिए वह नहीं जागताप्रेडिकेट-रहित प्रतीक्षा के लिए कोड खोजेंडंप से प्रतीक्षा थ्रेड पहचानेंif को while करें, या प्रेडिकेट waitखोया वेकअप या PulseEvent संदेह

चित्र 7: लक्षण “बहुत आगे बढ़ना” है या “कभी न जागना” दोनों संदेह और जाँच का तरीका बाँटता है।

यदि पुनरुत्पादित करना चाहें, मानक कदम रेस विंडो चौड़ा करना है। भौतिक कोर से अधिक थ्रेड उपयोग कर, प्रतीक्षा और सूचना के बीच जानबूझकर Sleep घुसाकर, और डिबग तथा रिलीज़ दोनों बिल्ड चलाकर समय जिटर बढ़ाएँ। जब पुष्टि हो कि “प्रेडिकेट-रहित प्रतीक्षा ठीक करने के बाद पुनरुत्पादन रुका”, उसी तनाव के अधीन तुलना करें।

8. सारांश — चेकलिस्ट

  • wait से वापस तीन पथ हैं — वास्तविक सूचना, स्प्यूरियस वेकअप, और चुराया वेकअप — और कॉलर उन्हें अलग नहीं कर सकता। इसलिए प्रतीक्षा हमेशा शर्त पर while लूप लिखें।
  • स्प्यूरियस वेकअप वह व्यवहार है जो Win32, C++, और POSIX ने प्रदर्शन के विरुद्ध समझौते के रूप में जानबूझकर अनुमति दी, और OS सुधार या लाइब्रेरी बदलने से नहीं जाएगा। .NET का Monitor.Wait बिना कारण जागने वाला नहीं माना जाता, पर क्योंकि चुराए वेकअप और टाइमआउट हैं, वही while अनुशासन फिर भी आवश्यक है।
  • C++ में, प्रेडिकेट रूप wait(lock, pred) डिफ़ॉल्ट करें। लाइब्रेरी लूप करती है।
  • शर्त अपडेट और जाँच एक ही लॉक के अधीन करें। सूचना “अवस्था अपडेट के बाद” भेजें। Win32/C++ सूचना लॉक छोड़ने के बाद हो सकती है; C# का Pulse केवल लॉक के भीतर।
  • टाइमआउट वाली प्रतीक्षा के लिए समय सीमा तय करें और शेष समय पुनः गणना करें। C++ में, wait_until प्लस प्रेडिकेट।
  • कंडीशन वेरिएबल की क्षणिक सूचना इवेंट पर पल्स से न दोहराएँ। विशेषकर PulseEvent वह है जिसके बारे में आधिकारिक दस्तावेज़ साफ़ शब्दों में कहता है “उपयोग न करें, इसके बजाय कंडीशन वेरिएबल उपयोग करें”। इवेंट स्वयं रोक निर्देश, WaitForMultipleObjects से जोड़ना, और प्रोसेस-पार सिंक्रोनाइज़ेशन के लिए सही उपकरण रहते हैं।
  • समीक्षा में, “प्रेडिकेट-रहित प्रतीक्षा” और “if + wait” यंत्रवत खोजें। दुर्लभ-पुनरुत्पादन बग को पुनरुत्पादन की प्रतीक्षा किए बिना मार सकते हैं।

स्प्यूरियस वेकअप, नाम की विचित्रता के विपरीत, सुधार के एक-पंक्ति कीवर्ड में संघनित होता है — if को while करें। और उस एक पंक्ति के पीछे कंडीशन-वेरिएबल उपकरण का डिज़ाइन विचार है: “सटीक सूचना महँगी है, इसलिए जाँचना प्रतीक्षाकर्ता की जिम्मेदारी है”। इसे तंत्र के रूप में समझें तो भाषा या फ़्रेमवर्क बदलने पर भी वही अनुशासन बिना हिचकिचाहट लागू कर सकेंगे।

संबंधित लेख

संबंधित परामर्श क्षेत्र

KomuraSoft LLC मल्टीथ्रेड डिज़ाइन समीक्षा, “केवल कभी-कभी पुनरुत्पादित” क्रैश और हैंग की मूल-कारण जाँच (डंप विश्लेषण), तथा लेगेसी सिंक्रोनाइज़ेशन कोड (इवेंट- और PulseEvent-निर्भर, आदि) को कंडीशन-वेरिएबल आधार पर माइग्रेट करना सँभालता है। लक्षण के ट्रायज से शुरू करना ठीक है — बेझिझक संपर्क करें।

संदर्भ लिंक

  1. Microsoft Learn, Condition Variables. कंडीशन वेरिएबल के यूज़र-मोड ऑब्जेक्ट होने पर जो लॉक परमाणु रूप से छोड़कर प्रतीक्षा में प्रवेश करता है; स्प्यूरियस वेकअप (स्पष्ट जाग से न बँधे जाग) और चुराए वेकअप (जागे थ्रेड से पहले दूसरे थ्रेड का चलना) होने पर, जिससे प्रतीक्षा से लौटने के बाद प्रेडिकेट while लूप में फिर जाँचना चाहिए; और सूचना लॉक के भीतर या बाहर से संभव होने, पर लॉक छोड़ने के बाद जगाने के कॉन्टेक्स्ट स्विच घटाने के लिए बेहतर होने पर।  2 3 4 5 6 7 8

  2. Microsoft Learn, SleepConditionVariableCS function (synchapi.h). निर्दिष्ट क्रिटिकल सेक्शन परमाणु रूप से छोड़कर कंडीशन वेरिएबल पर प्रतीक्षा करने पर; जागे थ्रेड के लौटने से पहले क्रिटिकल सेक्शन पुनः लेने पर; टाइमआउट पर ERROR_TIMEOUT लौटने पर; और स्प्यूरियस वेकअप तथा चुराए वेकअप होने पर, जिससे प्रतीक्षा से लौटने के बाद प्रेडिकेट (आमतौर पर while लूप में) फिर जाँचना चाहिए।  2

  3. The Open Group Base Specifications, pthread_cond_timedwait, pthread_cond_wait. pthread_cond_wait / pthread_cond_timedwait से स्प्यूरियस वेकअप हो सकने पर; प्रतीक्षा से लौटने का प्रेडिकेट के मान के बारे में कुछ न कहने पर, इसलिए प्रेडिकेट का पुनर्मूल्यांकन करना चाहिए; और Rationale के यह कहने पर कि “ठीक एक जगाओ” वाली इम्प्लीमेंटेशन कंडीशन-वेरिएबल ऑपरेशन विशेषकर मल्टीप्रोसेसर पर धीमे कर सकती है, और स्प्यूरियस वेकअप अनुमति देना प्रेडिकेट-जाँच लूप बाध्य कर ऐप अधिक मज़बूत बनाता है।  2 3

  4. cppreference.com, std::condition_variable::wait. प्रेडिकेट-रहित wait के स्प्यूरियस वेकअप से अनब्लॉक हो सकने पर; और प्रेडिकेट ओवरलोड के while (!pred()) wait(lock); के समकक्ष होने तथा हर सूचना या स्प्यूरियस वेकअप पर लॉक पुनः लेकर प्रेडिकेट जाँचने वाले लूप के रूप में परिभाषित होने पर।  2

  5. Microsoft Learn, condition_variable Class. प्रेडिकेट-रहित wait के notify_one / notify_all पर अनब्लॉक होने और स्प्यूरियस भी जाग सकने पर; प्रेडिकेट रूप wait(lock, pred) के प्रभावी रूप से while (!Pred()) wait(Lck); चलाने पर; और wait_for / wait_until के वही गुण तथा प्रेडिकेट ओवरलोड रखने पर।  2 3

  6. Microsoft Learn, Monitor.Wait Method. Wait के लॉक छोड़कर प्रतीक्षा कतार में प्रवेश करने पर; Pulse / PulseAll से जागने के बाद लॉक पुनः लेने तक न लौटने पर; और अभिप्रेत उपयोग के जागे थ्रेड के उस शर्त का पुनर्मूल्यांकन करने तथा यदि आवश्यक हो तो Wait फिर बुलाने पर जिसके कारण प्रतीक्षा में आया।  2

  7. Microsoft Learn, PulseEvent function (winbase.h). प्रतीक्षा थ्रेड के कर्नेल-मोड APC से प्रतीक्षा अवस्था से अस्थायी हटाए जा सकने और APC पूरा होने के बाद लौटने पर, जिससे उस अंतराल में PulseEvent बुलाए जाने पर थ्रेड मुक्त न होने पर; और PulseEvent के इसलिए अविश्वसनीय तथा नए ऐप में उपयोग न करने, इसके बजाय कंडीशन वेरिएबल उपयोग करने पर।  2

  8. Microsoft Learn, Using Condition Variables. एक क्रिटिकल सेक्शन और दो कंडीशन वेरिएबल (BufferNotEmpty और BufferNotFull) से उत्पादक–उपभोक्ता कतार लागू करने वाले आधिकारिक नमूने पर। प्रतीक्षा प्रेडिकेट जाँचने वाले लूप के भीतर की जाती है। 

  9. Microsoft Learn, WaitOnAddress function (synchapi.h). पते का मान बदलने की प्रतीक्षा करने वाली फ़ंक्शन के signaled होने पर लौटना गारंटीशुदा पर अन्य कारणों से लौटना भी अनुमति होने पर; जल्दी जागने के उदाहरणों में कम-मेमोरी स्थिति, उसी पते के लिए पिछला जाग त्यागना, और checked बिल्ड चलाना शामिल होने पर; और इसलिए लौटने के बाद मान फिर तुलना करने की आवश्यकता पर, आधिकारिक नमूना स्वयं while लूप होने पर। 

  10. Microsoft Learn, Monitor.PulseAll Method. PulseAll के थ्रेड प्रतीक्षा कतार से तैयार कतार पर ले जाने, और लॉक छूटने पर तैयार कतार के अगले थ्रेड के लॉक लेने पर; और Pulse / PulseAll / Wait के केवल सिंक्रोनाइज़ेशन ब्लॉक के भीतर से बुलाने योग्य होने पर।  2

  11. Microsoft Learn, Waits and APCs. कर्नेल APC के प्रीएम्प्टिव निष्पादन पर, और सिस्टम के आंतरिक रूप से प्रतीक्षा API से लौटे बिना प्रतीक्षा व्यवधान और फिर शुरू करने पर, जिससे KePulseEvent जैसा क्षणिक संकेत उस अंतराल में छूट सकता है। 

  12. Microsoft Learn, C28648: PulseEvent is an unreliable function. PulseEvent उपयोग पर स्टैटिक विश्लेषण चेतावनी पर; APC के कारण प्रतीक्षा से बाहर थ्रेड के मुक्त न होने और हमेशा हैंग रह सकने पर; और SetEvent या अन्य सिंक्रोनाइज़ेशन ऑब्जेक्ट से बदलने के मार्गदर्शन पर। 

निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।

DllMain और Loader Lock — DLL आरंभीकरण में "कुछ न करें" कहे जाने का वास्तविक कारण

DllMain से LoadLibrary क्यों न बुलाएँ या दूसरे थ्रेड से सिंक्रोनाइज़ न करें। प्राथमिक स्रोतों से यह लेख समझाता है कि लोडर लॉक हर DLL सूचन...

Named Pipes व्यवहार में — डिज़ाइन से सुरक्षा तक, Windows की मानक IPC

Named pipes — Windows की मानक इंटर-प्रोसेस कम्युनिकेशन — की व्यावहारिक मार्गदर्शिका। यह लेख प्राथमिक स्रोतों से बाइट और मैसेज मोड का चुना...

"Not Responding" वास्तव में क्या है — Windows ऐप के हैंग का निर्णय कैसे करता है, और न हैंग करने वाले ऐप कैसे डिज़ाइन करें

Windows का "Not Responding" वह तंत्र है जिसमें OS तय करता है कि विंडो ने 5 सेकंड संदेश नहीं लिया और उसे घोस्ट विंडो से बदल देता है। यह ले...

स्लीप से रिज़्यूम पर टूटने वाले ऐप — Windows पावर इवेंट कैसे काम करते हैं और उनसे बचने वाले व्यावसायिक ऐप कैसे बनाएँ

लैपटॉप खोला और व्यावसायिक ऐप के कनेक्शन मर चुके थे — कारण वह डिज़ाइन है जिसने स्लीप का हिसाब ही नहीं रखा। यह लेख WM_POWERBROADCAST सूचना ...

ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।

यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।

अक्सर पूछे जाने वाले प्रश्न

इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।

क्या स्प्यूरियस वेकअप OS या लाइब्रेरी का बग है?
नहीं — यह विनिर्देश में लिखा व्यवहार है। Win32 की SleepConditionVariableCS, C++ की std::condition_variable, और POSIX की pthread_cond_wait सबके आधिकारिक दस्तावेज़ या मानक स्पष्ट कहते हैं कि सूचना से न बँधा जागना हो सकता है। उसे मना करने वाली इम्प्लीमेंटेशन सैद्धांतिक रूप से संभव है, पर वह हर कंडीशन-वेरिएबल ऑपरेशन धीमा करेगी (विशेषकर मल्टीप्रोसेसर पर सूचना), इसलिए समझौता है इसे अनुमति देना इस समझ पर कि "यदि प्रतीक्षाकर्ता शर्त फिर जाँचे तो शुद्धता सुरक्षित रहती है"। इसलिए उपाय OS सुधार की प्रतीक्षा नहीं, प्रतीक्षा को हमेशा while लूप में लिखना है (या प्रेडिकेट-रूप प्रतीक्षा उपयोग करना)।
क्या प्रतीक्षा को while लूप में लपेटने से प्रदर्शन घटता है?
व्यवहार में लागत नगण्य है। while लूप केवल हर जागने पर एक अतिरिक्त शर्त जाँच जोड़ता है, और वह सस्ती तुलना है जब आप पहले से लॉक पकड़े हैं। स्प्यूरियस वेकअप स्वयं दुर्लभ हैं, इसलिए अतिरिक्त लूप चक्र केवल असाधारण स्थितियों में होता है। जाँच को if छोड़ने की लागत, दूसरी ओर, वह "बग जो केवल दुर्लभ पुनरुत्पादित होता है" है जिसमें शर्त अधूरी रहते प्रोसेसिंग आगे बढ़ती है — तुलना नहीं। कंडीशन-वेरिएबल प्रतीक्षा लागत पर वास्तव में हावी लॉक कॉन्टेंशन और कितनी बार सूचित करते हैं है, while है या नहीं नहीं।
यदि C++ की प्रेडिकेट-रूप प्रतीक्षा उपयोग करूँ, क्या स्प्यूरियस वेकअप भूल सकता हूँ?
प्रतीक्षा लूप के लिए, हाँ: cv.wait(lock, pred) प्रभावी रूप से while (!pred()) wait(lock); है, इसलिए स्प्यूरियस वेकअप और चुराए वेकअप दोनों स्वतः सोख लिए जाते हैं। नए C++ कोड को प्रेडिकेट ओवरलोड डिफ़ॉल्ट मानना चाहिए। आपको फिर भी प्रेडिकेट द्वारा पढ़ी साझा अवस्था के अपडेट उसी mutex से बचाने पड़ते हैं, और सूचित करने वाले को notify बुलाने से पहले वह अवस्था अपडेट करनी पड़ती है। प्रेडिकेट प्रतीक्षा लूप आपके हाथ से लेती है; लॉक अनुशासन नहीं।
क्या वही समस्या C# के Monitor.Wait से होती है?
हाँ। Monitor.Wait में प्रतीक्षा कर रहा थ्रेड Pulse/PulseAll से जागता है और फिर Wait से लौटने से पहले लॉक पुनः लेता है, पर उस अंतराल में दूसरा थ्रेड पहले लॉक ले शर्त खा सकता है (चुराया वेकअप)। Microsoft का दस्तावेज़ इस धारणा पर लिखा है कि जागा थ्रेड उस शर्त का पुनर्मूल्यांकन करता है जिसके कारण प्रतीक्षा की, और यदि आवश्यक हो तो Wait फिर बुलाता है। इसलिए C# में भी बुनियादी रूप while (!condition) Monitor.Wait(gate); है। Win32 से भिन्न एक बाधा है कि Wait/Pulse केवल lock कथन के भीतर से बुला सकते हैं।
क्या WaitForSingleObject से इवेंट की प्रतीक्षा पर भी स्प्यूरियस वेकअप होते हैं?
साधारण (गैर-alertable) प्रतीक्षा में, WAIT_OBJECT_0 केवल तब लौटता है जब ऑब्जेक्ट वास्तव में signaled हो; कंडीशन वेरिएबल जैसी "बिना कारण जाग" नहीं होती। इतना कहकर, "इवेंट signaled हुआ" और "आपके ऐप की शर्त पूरी है" अलग बातें हैं। यदि कई उपभोक्ता उसी इवेंट से जागें, पहले लॉक लेने वाला थ्रेड शर्त खाता है, इसलिए जागने के बाद शर्त फिर जाँचनी पड़ती है। जो डिज़ाइन कंडीशन वेरिएबल की "उस क्षण प्रतीक्षा कर रहे को ही जगाओ" क्षणिक सूचना इवेंट से दोहराने की कोशिश करते हैं वे PulseEvent की विश्वसनीयता समस्या में भी फँसते हैं, इसलिए प्रोसेस के भीतर शर्त की प्रतीक्षा के लिए कंडीशन वेरिएबल अधिक सुरक्षित उपकरण है।

लेखक की प्रोफ़ाइल

लेख के लेखक का परिचय पृष्ठ।

Go Komura

KomuraSoft LLC के प्रतिनिधि

Windows सॉफ़्टवेयर विकास, तकनीकी परामर्श और बग जाँच में विशेषज्ञ, विशेष रूप से मौजूदा सिस्टम वाली परियोजनाओं और पुनरुत्पादन में कठिन बग में।

सार्वजनिक लिंक

ब्लॉग पर लौटें