«मैं उपकरण नियंत्रण की निवासी प्रक्रिया C में लिख रहा हूँ।» «बीस साल पुराने C ऐप में थ्रेड जोड़ने पड़े।» «हम TerminateThread से थ्रेड रोकते हैं, पर कभी-कभी पूरी प्रक्रिया अटक जाती है।» — C में मल्टीथ्रेडिंग वह दुनिया है जहाँ भाषा सबसे कम मदद देती है। न अपवाद, न RAII, न टेम्पलेट; सिंक्रनाइज़ेशन की शुद्धता पूरी तरह इस पर टिकी है कि आप कौन से API चुनते हैं और उन्हें कितनी अनुशासन से बुलाते हैं।
यह लेख व्यावहारिक मल्टीथ्रेडिंग श्रृंखला का C संस्करण है। Win32 API पर C लिखने वाले डेवलपरों के लिए यह मल्टीथ्रेड डिज़ाइन के सिद्धांत — थ्रेडों का ढेर उत्पादन बंद करें, साझा परिवर्तनीय स्थिति घटाएँ, लॉक अनुशासन रखें, और शुरू करने से पहले रोकने का डिज़ाइन करें — Win32 के औज़ारों पर मैप करता है: थ्रेड कैसे बनाएँ (_beginthreadex), सिंक्रनाइज़ेशन ऑब्जेक्ट कैसे चुनें, TerminateThread के बिना स्टॉप पथ कैसे डिज़ाइन करें, और DllMain की सीमाएँ, अगस्त 2026 तक के प्राथमिक स्रोतों के इर्द-गिर्द संगठित। अकेले पढ़ने योग्य लिखा गया है। वही सिद्धांत, अन्य भाषाओं के लिए निकाले, .NET संस्करण, C++ संस्करण और Java संस्करण में भी हैं।
1. निष्कर्ष पहले
- थ्रेड
_beginthreadexसे बनाएँ,CreateThreadसे नहीं। यदि CRT बुलाने वाला थ्रेड CreateThread से बना हो, कम मेमोरी पर CRT प्रक्रिया समाप्त कर सकता है।12 - प्रक्रिया के भीतर डिफ़ॉल्ट लॉक SRW लॉक है; CRITICAL_SECTION केवल जब पुनरावर्ती अधिग्रहण चाहिए। प्रक्रिया के भीतर बहिष्करण के लिए Mutex «सामान्य गलती» है जो हमेशा कर्नेल मोड संक्रमण लाती है।3
- एकल वेरिएबल Interlocked परिवार से अपडेट करें।
volatileन अणुता गारंटी देता है न क्रम। अधिकतर Interlocked फ़ंक्शन पूर्ण मेमोरी बैरियर लाते हैं।4 - प्रतीक्षा कंडीशन वेरिएबल (
SleepConditionVariableCSपरिवार) या इवेंट प्लस प्रतीक्षा फ़ंक्शन से करें।Sleepआधारित पोलिंग लूप CPU और प्रतिक्रिया दोनों बर्बाद करता है।5 TerminateThreadकभी न इस्तेमाल करें। यह खतरनाक फ़ंक्शन लॉक, हीप और DLL स्थिति भ्रष्ट करता है, और कोड-विश्लेषण चेतावनी C6258 का लक्ष्य है। रोक को सहकारी शटडाउन डिज़ाइन करें: स्टॉप इवेंट प्लसWaitForMultipleObjects।67- छोटे काम Windows थ्रेड पूल (
CreateThreadpoolWork) को सौंपें, अपने थ्रेडों से समांतर न करें। पूल थ्रेड कभीExitThread/TerminateThreadसे न खत्म करें।89 - DllMain में थ्रेड न बनाएँ, सिंक्रनाइज़ न करें, थ्रेड खत्म होने की प्रतीक्षा न करें। इसे लोडर लॉक पकड़े बुलाया जाता है, इसलिए यह डेडलॉक का अड्डा है।10
- C11 का
<threads.h>VS 2022 17.8 से प्रयोग योग्य है, पर<stdatomic.h>अभी प्रायोगिक है। केवल-Windows कोडबेस के लिए Win32 तरीका यथार्थवादी चुनाव है।11
2. मल्टीथ्रेडिंग कठिन क्यों है? — रेस कंडीशन और डेडलॉक
मल्टीथ्रेडिंग जो समस्याएँ लाती है उन्हें भाषा निरपेक्ष उबालें तो दो तरह की बचती हैं।
रेस कंडीशन वह बग है जिसमें परिणाम इस पर बदलता है कि कई थ्रेड किसी कोड टुकड़े तक किस क्रम में पहुँचते हैं। क्लासिक उदाहरण साझा काउंटर है: एकल व्यंजक count++ मशीन-कोड स्तर पर तीन चरणों में टूटता है — पढ़ो, जोड़ो, वापस लिखो। यदि दो थ्रेड इन तीन चरणों में एक साथ प्रवेश करें, एक थ्रेड का जोड़ दूसरे के वापस लिखने पर अधिलेखित होकर खो जाता है।4 परिणाम रन से रन बदलता है, और कौन सा परिणाम मिलेगा अप्रत्याशित है।
sequenceDiagram
participant A as थ्रेड A
participant M as साझा वेरिएबल count
participant B as थ्रेड B
Note over M: count = 10
A->>M: पढ़ना (10)
B->>M: पढ़ना (10)
A->>A: स्थानीय जोड़ (11)
B->>B: स्थानीय जोड़ (11)
A->>M: वापस लिखना (11)
B->>M: वापस लिखना (11)
Note over M: दो वृद्धि के बावजूद count = 11<br/>थ्रेड A की वृद्धि खो गई
चित्र 1: क्लासिक रेस कंडीशन जिसमें साझा काउंटर एक वृद्धि खो देता है। यदि count++ के तीन चरणों में दूसरा थ्रेड बीच में आए, जो वापस-लेखन बाद में होता है वही दूसरे को अधिलेखित करता है
डेडलॉक वह अवस्था है जिसमें दो थ्रेड प्रत्येक उस लॉक की प्रतीक्षा करते हैं जो दूसरा पकड़े है, और कोई आगे नहीं बढ़ सकता। थ्रेड A लॉक 1 पकड़े लॉक 2 की प्रतीक्षा करता है; थ्रेड B लॉक 2 पकड़े लॉक 1 की प्रतीक्षा करता है — इतना काफी है कि दोनों हमेशा के लिए रुक जाएँ।
flowchart LR
A["थ्रेड A<br/>लॉक 1 पकड़े"] -->|"लॉक 2 की रिहाई की प्रतीक्षा"| B["थ्रेड B<br/>लॉक 2 पकड़े"]
B -->|"लॉक 1 की रिहाई की प्रतीक्षा"| A
चित्र 2: डेडलॉक की वृत्ताकार प्रतीक्षा। जिस क्षण प्रतीक्षा तीर वलय बनाते हैं, उस वलय का हर थ्रेड हमेशा के लिए रुक जाता है
दोनों समय-निर्भर हैं: निष्पादन-क्रम संयोजन जो विकास मशीन पर दसियों हज़ार रन में एक बार दिखे, अलग कोर संख्या और समय वाली ग्राहक मशीन पर रोज़ हो सकता है। «डिबगर लगा तो पुनरुत्पादित नहीं होता» इसलिए होता है कि अवलोकन स्वयं समय बदल देता है — रेस बग का विशिष्ट व्यवहार। इसलिए इस लेख का हर सिद्धांत एक दिशा में है: सही सिंक्रनाइज़ करने की चिंता से पहले वे जगहें घटाएँ जहाँ सिंक्रनाइज़ेशन चाहिए।
2.1. C-विशिष्ट मान्यताएँ — भाषा आपको किसी चीज़ से नहीं बचाती
C में, क्योंकि भाषा के पास इन सिद्धांतों को लागू करने की व्यवस्था नहीं, उन्हें अनुशासन के रूप में स्पष्ट लिखना पड़ता है।
पहला, रिहाई की गारंटी संरचना में बनाएँ। C++ के RAII के समकक्ष कुछ न होने पर, लॉक रिहाई और हैंडल पर CloseHandle को goto cleanup पैटर्न से बचाना होता है जो हर फ़ंक्शन निकास एक जगह से निकालता है, या कोडिंग परिपाटी से जो हर अधिग्रहण को रिहाई से जोड़ती है। जल्दी return जोड़कर लॉक लीक करना C की क्लासिक दुर्घटना है।
दूसरा, डेटा रेस के साथ C++ जैसा व्यवहार करें। सही संरेखित 32-बिट वेरिएबल का सादा पढ़ना या लिखना Windows पर अणु है, पर उससे आगे — 32-बिट Windows पर 64-बिट वेरिएबल, संयुक्त संक्रियाएँ, या कई वेरिएबलों में संगति — कुछ भी गारंटी नहीं।12 «संयोग से चलता» कोड कंपाइलर या अनुकूलन स्तर बदलते ही टूटता है।
तीसरा, स्वामित्व तय करें। फ़ंक्शन टिप्पणी में «यह बफ़र कौन सा थ्रेड लिखता है, और कब से किसका है» स्पष्ट करने की संस्कृति C मल्टीथ्रेडिंग में सिंक्रनाइज़ेशन प्रिमिटिव के चुनाव जितनी ही फलती है।
3. थ्रेड कैसे बनाएँ — _beginthreadex, और कुछ नहीं
3.1. CreateThread गलत चुनाव क्यों है
Win32 का नेटिव API CreateThread है, पर आधिकारिक मार्गदर्शन है कि कोई भी थ्रेड जो CRT (C रनटाइम) फ़ंक्शन बुलाए उसे _beginthreadex से बनाना चाहिए। _beginthreadex थ्रेड शुरू करने से पहले CRT को प्रति-थ्रेड चाहिए आंतरिक डेटा आरंभ करता है। यदि CreateThread से बना थ्रेड CRT फ़ंक्शन बुलाए, कम-मेमोरी स्थिति में CRT प्रक्रिया समाप्त कर सकता है।12 चूँकि printf, malloc और strtok सभी CRT फ़ंक्शन हैं, व्यावहारिक नियम: «C में लिखे थ्रेड हमेशा _beginthreadex इस्तेमाल करें»।
_beginthread (बिना ex) से भी बचें। उसमें जाल है: यदि बनाया थ्रेड जल्दी खत्म हो, लौटा हैंडल पहले से अमान्य हो सकता है — या दूसरे थ्रेड की ओर इशारा भी कर सकता है — जबकि _beginthreadex, जिसका हैंडल सिंक्रनाइज़ेशन API को सुरक्षित दिया जा सकता है, सुरक्षित चुनाव है। कॉलर _beginthreadex का लौटा हैंडल CloseHandle से बंद करता है।13
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... खंड 5 और 6 का प्रतीक्षा लूप ... */
return 0;
}
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* विफलता संभालना */ }
/* ...स्टॉप अनुरोध के बाद... */
WaitForSingleObject(hThread, INFINITE); /* जॉइन */
CloseHandle(hThread);
3.2. छोटे काम Windows थ्रेड पूल को जाएँ
यदि आप «बहुत से छोटे काम किसी चीज़ पर फेंकना» चाहते हैं या «बार-बार अल्पकालिक थ्रेड बना-मिटा रहे» हैं, अपने थ्रेडों की जगह Windows थ्रेड पूल (Vista से उपलब्ध थ्रेड पूल API) इस्तेमाल करें। CreateThreadpoolWork से वर्क ऑब्जेक्ट बनाएँ और SubmitThreadpoolWork से जमा करें, और पूल के वर्कर थ्रेड कॉलबैक समांतर चलाते हैं।8 थ्रेड संख्या का प्रबंधन OS के पास रहता है, और थ्रेड बनाने-मिटाने की लागत गायब हो जाती है। «अपने थ्रेड न बनाएँ» सिद्धांत का C उत्तर यही है।
पूल इस्तेमाल का अनुशासन भी आधिकारिक रूप से दर्ज है: पूल थ्रेड कभी TerminateThread / ExitThread से न खत्म करें; कॉलबैक में बदली कोई स्थिति (TLS, थ्रेड प्राथमिकता आदि) लौटने से पहले बहाल करें; और प्रतीक्षा हैंडल पूल के इस्तेमाल खत्म होने तक जीवित रखें।9 एक और व्यावहारिक नोट: पूल केवल वर्कर थ्रेडों की संख्या सीमित करता है — SubmitThreadpoolWork से कतारबद्ध अनिष्पादित कॉलबैक बिना सीमा जमा हो सकते हैं। लंबी-चलती बनावट में जहाँ जमाव प्रसंस्करण से आगे रहता है, ऐप पक्ष पर सेमाफोर जैसा प्रवेश सीमितक, या सीमाबद्ध कतार रखें, ताकि जमा करने वाला पक्ष भर जाने पर प्रतीक्षा करे या अस्वीकृत हो (यह बैक-प्रेशर है ताकि अधिभार मेमोरी समस्या न बन जाए, और खंड 5 के कतार डिज़ाइन जैसा ही सिद्धांत)।
4. साझा परिवर्तनीय स्थिति न्यूनतम करें — विभाजन, केवल-पठन, और सौंपना
रेस तभी होती है जब «कई थ्रेड» और «साझा परिवर्तनीय डेटा» दोनों मौजूद हों। सिंक्रनाइज़ेशन प्रिमिटिव चुनने (अगला अध्याय) से पहले सोचें कि साझा करना पहले ही घटाया जा सकता है या नहीं। तीन परिवार की तकनीकें हैं।
विभाजित करें। समांतर संकलन में हर थ्रेड साझा काउंटर पर लिखने के बजाय प्रति-थ्रेड स्थानीय वेरिएबल (या प्रति थ्रेड आवंटित बफ़र) में उपयोग बनाएँ, और अंत में एक बार InterlockedAdd जैसी चीज़ से मिलाएँ। साझा स्थिति पर लेखन «हर पुनरावृत्ति» से «प्रति थ्रेड एक बार» गिरता है, और सिंक्रनाइज़ेशन लागत व प्रतिस्पर्धा खिड़की दोनों परिमाण क्रम से सिकुड़ते हैं। खंड 2.1 का स्वामित्व अनुशासन — «यह बफ़र किस थ्रेड का है» — विभाजन का नक्शा बन जाता है।
केवल-पठन बनाएँ। स्टार्टअप पर बनी और बाद में न बदली गई कॉन्फ़िगरेशन और तालिकाएँ आरंभीकरण पूरा होने के बाद किसी भी संख्या के थ्रेड से पढ़ना सुरक्षित हैं। या तो सारे थ्रेड शुरू होने से पहले सारा आरंभीकरण खत्म करें, या यदि आलसी आरंभीकरण चाहिए तो Win32 का एक-बार आरंभीकरण (InitOnceExecuteOnce) इस्तेमाल करें, और सीमा — «कब से यह केवल-पठन हो जाता है» — कोड में स्पष्ट करें।3
सौंपें। दोनों पक्षों को साझा वेरिएबल छूने देने के बजाय, थ्रेडों के बीच डेटा प्रवाह निर्माता/उपभोक्ता कतार से निकालें। C में कार्यान्वयन ठीक खंड 5 का कंडीशन-वेरिएबल पैटर्न है (सीमाबद्ध वृत्ताकार बफ़र प्लस SleepConditionVariableCS), जो आधिकारिक कार्यित उदाहरण है; क्षमता सीमा वाला बफ़र स्वाभाविक बैक-प्रेशर भी देता है — उत्पादन खपत से आगे निकलते ही प्रतीक्षा करता है।5
5. सिंक्रनाइज़ेशन ऑब्जेक्ट चुनना और लॉक अनुशासन
Win32 में कई तरह के सिंक्रनाइज़ेशन प्रिमिटिव हैं, और गलत चुनाव प्रदर्शन और शुद्धता दोनों का दाम लेता है। आधिकारिक मार्गदर्शन एक चित्र में संक्षेप।3
flowchart TB
S{"प्रक्रियाओं के आर-पार<br/>सिंक्रनाइज़ेशन चाहिए?"} -->|"हाँ"| Q2{"किस लिए?"}
Q2 -->|"पारस्परिक बहिष्करण"| MTX["नामित Mutex"]
Q2 -->|"समवर्ती पहुँच संख्या सीमित करना"| SEM["नामित सेमाफोर"]
Q2 -->|"इवेंट सूचना"| EVT["नामित इवेंट"]
S -->|"नहीं - प्रक्रिया-आंतरिक"| Q3{"उसी थ्रेड से पुनरावर्ती<br/>अधिग्रहण चाहिए?"}
Q3 -->|"हाँ"| CS["CRITICAL_SECTION"]
Q3 -->|"नहीं"| Q4{"पोर्टेबल C++ कोड<br/>प्राथमिकता?"}
Q4 -->|"हाँ"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"नहीं"| SRW["SRW लॉक - डिफ़ॉल्ट चुनाव"]
चित्र 3: Win32 सिंक्रनाइज़ेशन प्रिमिटिव कैसे चुनें। पहली शाखा «क्या प्रक्रियाओं को पार करता है» — मुद्दा यह है कि जब नहीं करता तो कर्नेल ऑब्जेक्ट (Mutex) न उठाएँ
| प्रिमिटिव | दायरा | विशेषताएँ | कहाँ इस्तेमाल करें |
|---|---|---|---|
| SRW लॉक | प्रक्रिया-आंतरिक | तेज़ (आमतौर पर पूरा यूज़र मोड में रहता है), पॉइंटर आकार, अपुनरावर्ती | नए कोड का डिफ़ॉल्ट। AcquireSRWLockShared साझा पठन पहुँच भी देता है |
| CRITICAL_SECTION | प्रक्रिया-आंतरिक | तेज़ (घुमता है, फिर कर्नेल प्रतीक्षा में गिरता है), पुनरावर्ती | जब उसी थ्रेड को पुनरावर्ती अधिग्रहण चाहिए |
| Mutex | प्रक्रिया-आंतरिक / प्रक्रियाओं के आर-पार | हमेशा कर्नेल ऑब्जेक्ट, इसलिए धीमा | प्रक्रियाओं के आर-पार बहिष्करण (नामित), या WaitForMultipleObjects के साथ |
| सेमाफोर | प्रक्रिया-आंतरिक / प्रक्रियाओं के आर-पार | कर्नेल ऑब्जेक्ट | संसाधन पूल पर समवर्ती पहुँच सीमित करना |
| इवेंट | प्रक्रिया-आंतरिक / प्रक्रियाओं के आर-पार | कर्नेल ऑब्जेक्ट | सूचित करना कि «कुछ हुआ» (डेटा बचाने के लिए नहीं) |
| Interlocked फ़ंक्शन | प्रक्रिया-आंतरिक (साझा मेमोरी पर प्रक्रियाओं के आर-पार भी) | लॉक-मुक्त अणु संक्रियाएँ | काउंटर, फ़्लैग, पॉइंटर अदला-बदली4 |
तालिका और फ्लोचार्ट की एक पादटिप्पणी। इवेंट, सेमाफोर और म्यूटेक्स जैसे कर्नेल ऑब्जेक्ट बिना नाम बनाए प्रक्रिया-आंतरिक सिंक्रनाइज़ेशन के लिए बिल्कुल काम करते हैं (खंड 6 का स्टॉप इवेंट ठीक एक अনাमित इवेंट है)। कर्नेल ऑब्जेक्ट का अर्थ केवल प्रक्रियाओं के आर-पार नहीं। न ही, उलटा, «अनामित» सख्ती से «एक प्रक्रिया तक सीमित» है — यदि आप चाइल्ड प्रक्रिया को हैंडल विरासत दें, या DuplicateHandle से दूसरी प्रक्रिया में नकल करें, वही कर्नेल ऑब्जेक्ट बिना नाम भी कई प्रक्रियाओं से इस्तेमाल हो सकता है। सटीक कहना यह है कि नामकरण प्रक्रियाओं को वही ऑब्जेक्ट फिर खोलने देने का एक प्रतिनिधि तरीका है। चित्र 3 की शाखा चुनाव का मुद्दा «प्रक्रिया-आंतरिक लॉक के लिए कर्नेल ऑब्जेक्ट न चुनें» पकड़ती है — प्रक्रिया-आंतरिक सूचना (इवेंट) या समवर्तीता सीमा (सेमाफोर) के लिए अनामित कर्नेल ऑब्जेक्ट अभी भी सही उत्तर है।
Interlocked परिवार .NET संस्करण की Interlocked कक्षा और C++ संस्करण के std::atomic से मेल खाता है। InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange एक वेरिएबल पर संक्रिया अविभाज्य करते हैं, और चूँकि इनमें से अधिकतर पूर्ण मेमोरी बैरियर लाते हैं, क्रम की गारंटी भी मिलती है।4 «ठीक है क्योंकि मैंने volatile लगाया» भ्रम है — volatile न अणुता गारंटी देता है न क्रम (FAQ देखें)। एक और पूर्वापेक्षा: संरेखण। Interlocked फ़ंक्शन का लक्ष्य वेरिएबल प्राकृतिक सीमा पर संरेखित होना चाहिए (32-बिट मान के लिए 4-बाइट सीमा, 64-बिट के लिए 8-बाइट); नहीं तो व्यवहार अप्रत्याशित है।12 #pragma pack वाली struct के फ़ील्ड, या तार प्रारूप पर सीधे मैप बफ़र के फ़ील्ड को Interlocked का लक्ष्य कभी न बनाएँ। काउंटर और फ़्लैग साधारण घोषित वेरिएबल तक सीमित रखें — जिन्हें कंपाइलर आपके लिए संरेखित करता है। पॉइंटर अदला-बदली जैसे InterlockedExchangePointer पर विशिष्ट चेतावनी भी है: अविभाज्य केवल अदला-बदली स्वयं है, और अदला-बदली के बाद पुराने ब्लॉक के जीवन की गारंटी कोई नहीं देता। यदि पाठक पुराना पॉइंटर लोड करे ठीक जब लेखक उसे बदलकर free बुलाए, वह मुक्त मेमोरी पर पहुँच है। पॉइंटर अदला-बदली से साझा डेटा अपडेट करने वाला डिज़ाइन तभी चलता है जब पुनर्ग्रहण प्रोटोकॉल — लॉक, संदर्भ गिनती या समान — के साथ जोड़ा हो (संदेह हो तो SRW लॉक से बचाना सुरक्षित डिफ़ॉल्ट है)।
किसी चीज़ की प्रतीक्षा के लिए कंडीशन वेरिएबल हैं। InitializeConditionVariable से एक बनाएँ; उपभोक्ता SleepConditionVariableCS पर सोता है (CRITICAL_SECTION के साथ जोड़ा), निर्माता WakeConditionVariable से जगाता है — यही सीमाबद्ध बफ़र पर आधिकारिक निर्माता/उपभोक्ता कतार उदाहरण का आकार है।5 महत्वपूर्ण अनुशासन: जागने पर, लॉक के भीतर शर्त (क्या कतार खाली नहीं) हमेशा फिर जाँचें, और झूठ हो तो प्रतीक्षा पर लौटें। कंडीशन वेरिएबल बिना किसी सूचना के स्प्यूरियस वेकअप झेल सकते हैं, और जब आप जागें तब तक दूसरा उपभोक्ता आइटम पहले ले चुका हो — इसलिए «मुझे जगाया गया» जरूरी नहीं «शर्त सत्य है»। यह .NET संस्करण खंड 4.3 के चैनल और C++ संस्करण खंड 4 की BlockingQueue जैसा आकार C में बनाने का औज़ार है। SRW लॉक से जोड़ते समय SleepConditionVariableSRW इस्तेमाल करें।
5.1. लॉक अनुशासन — तीन सिद्धांत जो कोई भी प्रिमिटिव चुनें टिकते हैं
सही प्रिमिटिव चुनना अकेले काफी नहीं — अनुशासित इस्तेमाल के बिना रेस फिर भी नहीं रुकेंगी।
- एक-से-एक तय करें कौन सा लॉक कौन सा डेटा बचाता है। हर उस परिवर्तनीय डेटा समूह को ठीक एक लॉक (SRW लॉक या CRITICAL_SECTION) दें जिसे बचाना चाहते हैं, और उस डेटा को छूने वाली हर जगह वही लॉक लें। C में खासकर हेडर टिप्पणी में स्पष्ट लिखना फलता है — «इस struct की रक्षा
g_lockFooकरता है»। - लॉक पकड़े कुछ धीमा या बाहरी न करें। लॉक पकड़े केवल वही ठीक है जो वह डेटा पढ़ना या लिखना जिसे वह बचाता है। लॉक अभी पकड़े फ़ाइल I/O, नेटवर्क कॉल या कॉलबैक आह्वान पकड़ने का समय बढ़ाते हैं और जोखिम है कि बुलाया गया दूसरा लॉक लेने की कोशिश करे, चित्र 2 की वृत्ताकार प्रतीक्षा बनाता हुआ।
- कई लॉकों का अधिग्रहण क्रम स्थिर करें। जहाँ दो या अधिक लॉक लिए जाएँ, नियम बनाएँ कि हर थ्रेड उन्हें उसी क्रम में ले (लॉक पदानुक्रम)। DLL सर्वोत्तम-अभ्यास दस्तावेज़ स्पष्ट कहता है कि उस क्रम को पलटना (lock order inversion) ऐसे डेडलॉक पैदा करता है जिन्हें डीबग करना कठिन है, और पदानुक्रम परिभाषित कर लगातार उसका पालन करना चाहिए।10
6. रोकने का डिज़ाइन — कभी TerminateThread नहीं
6.1. TerminateThread क्या तोड़ता है
TerminateThread लक्ष्य थ्रेड को कोई यूज़र-मोड कोड चलाने दिए बिना मिटा देता है। आधिकारिक दस्तावेज़ जो परिणाम गिनाता है वे गंभीर हैं। यदि लक्ष्य थ्रेड क्रिटिकल सेक्शन पकड़े था, वह कभी नहीं छूटता; यदि हीप संक्रिया के बीच था, हीप लॉक पकड़ा रहता है (और बाद का हर थ्रेड जो malloc बुलाए अटकता है); और यदि किसी DLL की ग्लोबल स्थिति बदल रहा था, वह स्थिति भ्रष्ट रह जाती है। आधिकारिक स्थिति है कि यह «एक खतरनाक फ़ंक्शन है जिसका उपयोग केवल सबसे चरम मामलों में होना चाहिए», और कोड विश्लेषण इसे चेतावनी C6258 लगाता है।67
«कभी-कभी पूरी तरह अटकने» वाले ऐप की जाँच में TerminateThread मिलना व्यवहार में सचमुच आम दृश्य है। मिले तो उसे ठीक करने योग्य समझें।
6.2. सही पैटर्न: स्टॉप इवेंट प्लस WaitForMultipleObjects
C में सहकारी शटडाउन का स्थापित पैटर्न एक मैनुअल-रीसेट स्टॉप इवेंट बनाना है, और हर वर्कर थ्रेड को «काम संकेत» और «स्टॉप संकेत» एक साथ प्रतीक्षा कराना। C6258 चेतावनी दस्तावेज़ स्वयं ठीक इसी पैटर्न की ओर इशारा करता है — इवेंट बनाएँ, हर थ्रेड WaitForSingleObject से उसकी निगरानी करे, और थ्रेड स्वयं खत्म हो — सही समाप्ति के रूप में।7
HANDLE hStopEvent; /* CreateEvent(NULL, TRUE, FALSE, NULL): मैनुअल-रीसेट */
HANDLE hWorkEvent; /* CreateEvent(NULL, FALSE, FALSE, NULL): ऑटो-रीसेट.
प्राप्त होते ही स्वतः गैर-संकेतित पर लौटता है
(मैनुअल-रीसेट इवेंट एक बार संकेत के बाद
प्रतीक्षाएँ सीधी पार कर देता, खाली कतार पर
घूमता व्यस्त लूप बन जाता) */
static unsigned __stdcall WorkerMain(void* arg)
{
HANDLE waits[2] = { hStopEvent, hWorkEvent };
for (;;) {
DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (r == WAIT_FAILED) { /* जैसे अमान्य हैंडल. बिना संभाले यह खाली घूमता है */
LogLastError(); /* GetLastError() लिखें और निकलें */
break;
}
if (r == WAIT_OBJECT_0) /* स्टॉप अनुरोध */
break;
if (r == WAIT_OBJECT_0 + 1) { /* काम उपलब्ध */
/* ProcessNextItem को भी स्टॉप इवेंट दें: यदि वह एक आइटम के लिए
भीतर देर प्रतीक्षा करे, और वहाँ भी स्टॉप न देख सके,
शटडाउन उस एक आइटम का बंधक हो जाता है */
while (ProcessNextItem(hStopEvent)) { /* कतार से एक आइटम संसाधित; खाली हो तो FALSE */
/* निकालते समय भी स्टॉप अनुरोध जाँचें. इसे छोड़ें तो काम
जमा रहता है तो रुक नहीं सकते (स्टॉप भुखमरी) */
if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
break;
}
}
}
Cleanup(); /* अपनी सफ़ाई स्वयं करें */
return 0; /* स्वयं खत्म हों */
}
BOOL StopWorkers(HANDLE* threads, DWORD count)
{
BOOL ok = TRUE;
if (!SetEvent(hStopEvent)) { /* यदि स्टॉप अनुरोध नहीं पहुँचा, */
LogLastError(); /* असीमित जॉइन पर न जाएँ */
return FALSE;
}
/* अब सबके पास स्टॉप अनुरोध एक साथ उठा है */
for (DWORD i = 0; i < count; i++) {
if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
CloseHandle(threads[i]); /* केवल वे हैंडल बंद करें जिनका जॉइन पुष्टि हुआ */
} else {
LogLastError(); /* WAIT_FAILED: जैसे अमान्य हैंडल */
ok = FALSE; /* «सब रुके» न बताएँ */
}
}
return ok; /* FALSE हो तो साझा संसाधन छोड़ने पर न बढ़ें */
}
रोकने वाला पक्ष WaitForSingleObject से एक-एक थ्रेड जॉइन करे, इसका कारण है। WaitForMultipleObjects एक साथ अधिकतम MAXIMUM_WAIT_OBJECTS (64) हैंडल की प्रतीक्षा कर सकता है; बड़ा ऐरे दें तो प्रतीक्षा स्वयं WAIT_FAILED से असफल हो जाती है, और आप हैंडल बंद करते रह जाते हैं यह मानकर कि सबकी प्रतीक्षा की जबकि किसी की नहीं की। यदि केवल सबके खत्म होने की प्रतीक्षा चाहिए, बिना ऊपरी सीमा के एक-एक लूप सुरक्षित चुनाव है।
flowchart TB
OWNER["रोकने वाला"] -->|"SetEvent(hStopEvent)"| SE["स्टॉप इवेंट<br/>मैनुअल-रीसेट - सबको एक साथ दिखे"]
SE --> W1["वर्कर 1 - स्टॉप और काम की<br/>एक साथ प्रतीक्षा WaitForMultipleObjects से"]
SE --> W2["वर्कर 2 - स्टॉप और काम की<br/>एक साथ प्रतीक्षा WaitForMultipleObjects से"]
W1 --> C1["साफ़ करके स्वयं लौटता है"]
W2 --> C2["साफ़ करके स्वयं लौटता है"]
C1 --> J["रोकने वाला थ्रेड हैंडल की प्रतीक्षा कर जॉइन करता है<br/>अब ही रुका कह सकते हैं"]
C2 --> J
चित्र 4: स्टॉप-इवेंट पैटर्न। स्टॉप संकेत के लिए मैनुअल-रीसेट इवेंट का अर्थ है एक SetEvent हर प्रतीक्षारत वर्कर को एक साथ जगाता है। हर थ्रेड स्वयं तय करता है कैसे खत्म हो, और रोक तभी पूर्ण मानी जाती है जब जॉइन पूरा हो
तीन मुख्य बिंदु हैं। स्टॉप इवेंट मैनुअल-रीसेट बनाएँ (ताकि एक SetEvent हर वर्कर को दिखे); प्रतीक्षा ऐरे में स्टॉप इवेंट पहले रखें (ताकि दोनों एक साथ संकेतित हों तो स्टॉप को प्राथमिकता मिले); और रोकने वाला हमेशा हैंडल बंद करने से पहले थ्रेड हैंडल जॉइन करे।
दायरे की दो चेतावनियाँ। पहला, यह «इवेंट प्लस सब-निकालो» पैटर्न एकल-वर्कर बनावट के लिए है। ऑटो-रीसेट इवेंट पर कितनी भी बार SetEvent बुलाएँ, वह केवल «एक संकेतित अवस्था है» व्यक्त कर सकता है (लगातार संकेत मिल जाते हैं), इसलिए कई वर्कर हों तो एक जागता है और पूरा विस्फोट क्रमिक संसाधित करता है। यदि कई वर्कर कतार साझा करें, काम संकेत सेमाफोर पर बदलें, हर आइटम कतार में रखते ReleaseSemaphore(hSem, 1, NULL) से गिनती बढ़ाएँ। सफल सेमाफोर प्रतीक्षा एक गिनती खाती है, सही संगतता देती हुई: कतारित आइटमों जितने प्रतीक्षारत वर्कर एक-एक जागते हैं (यह उपयोग सेमाफोर के कार्यक्षेत्र में है, चित्र 3 तालिका के «संसाधन पूल पर समवर्ती पहुँच सीमित करना» जैसा)। पर सेमाफोर पर जाते समय उपभोक्ता पक्ष भी बदलें ताकि एक सफल प्रतीक्षा ठीक एक आइटम संसाधित करने के बराबर हो। ऊपर के नमूने का सब-निकालो लूप ज्यों का त्यों छोड़ें, और एक प्रतीक्षा — जो केवल एक अनुमति खाती है — पूरी कतार खाली कर देगी, हिसाब बिगाड़ते हुए: बची अनुमतियों पर अन्य वर्कर खाली कतार पर जागते हैं, और निर्माता का ReleaseSemaphore अधिकतम गिनती पार करने से असफल होने लगता है। «एक अनुमति बराबर एक काम» संगतता रखना सेमाफोर दृष्टिकोण की पूर्वापेक्षा है। दूसरा, स्टॉप पथ एक आइटम के प्रसंस्करण से भी निकालें। यदि ProcessNextItem भीतर लंबी अवरोधक प्रतीक्षा करे, या वहाँ भी स्टॉप इवेंट दें और दोनों की साथ प्रतीक्षा करें, या परिमित टाइमआउट लगाएँ। केवल आइटमों के बीच जाँच «एक आइटम न खत्म होने से शटडाउन हमेशा प्रतीक्षा करे» का छेद छोड़ती है। यह वही बात कहता है जो .NET संस्करण का StopAsync और C++ संस्करण का jthread प्लस जॉइन।
अवरोधक I/O (पाइप, सॉकेट, सीरियल पोर्ट) पर प्रतीक्षारत थ्रेड इवेंट जाँचने नहीं लौट सकता, इसलिए I/O पक्ष को अपना डिज़ाइन चाहिए — या OVERLAPPED I/O इवेंट के साथ जिसे साथ प्रतीक्षा करें, या CancelIoEx से I/O जगाएँ (ठोस सीरियल उदाहरण के लिए “Serial Communication App Pitfalls - Through Reconnection and Log Design” देखें)।
7. DllMain और लोडर लॉक — DLL लेखकों के लिए बारूदी सुरंग
C में लिखे साझा घटक अक्सर DLL बन जाते हैं, और DLL की अपनी बाधा है: लोडर लॉक। OS लोडर DllMain को लोडर लॉक पकड़े बुलाता है, इसलिए इसमें निम्न में से कुछ भी करना डेडलॉक या क्रैश का स्रोत बनता है।10
- अन्य थ्रेडों से सिंक्रनाइज़ करना (लॉक लेना, थ्रेड खत्म होने की प्रतीक्षा)
LoadLibrary/FreeLibraryबुलाना, सीधे या अप्रत्यक्ष- थ्रेड बनाना (सिंक्रनाइज़ेशन हो तो खतरनाक), या
ExitThreadबुलाना
«DLL अनलोड होते वर्कर थ्रेड खत्म होने की DllMain में प्रतीक्षा» बिल्कुल उचित दिखती है पर क्लासिक डेडलॉक है: खत्म होता थ्रेड DLL_THREAD_DETACH पहुँचाने लोडर लॉक लेने की कोशिश करता है, और दोनों पक्ष एक-दूसरे की प्रतीक्षा करते रह जाते हैं। अपने थ्रेडों वाली DLL को स्पष्ट आरंभीकरण और शटडाउन फ़ंक्शन — जैसे MyLib_Init / MyLib_Shutdown — उजागर करने चाहिए, और थ्रेड स्टार्ट व जॉइन वहीं करने चाहिए। आदर्श DllMain खाली स्टब के निकट है।10
8. C11 थ्रेड विकल्प — स्थिति कहाँ है
यदि Win32 पर निर्भर न रहने वाला पोर्टेबल C लिखना हो, विकल्प C11 का <threads.h> (thrd_create / mtx_lock / cnd_wait) और <stdatomic.h> है। आधिकारिक अनुरूपता तालिका के अनुसार MSVC समर्थन यह है: <threads.h> Visual Studio 2022 17.8 से समर्थित (/std:c11 और मिलता Windows SDK चाहिए), जबकि <stdatomic.h> अभी प्रायोगिक है, /experimental:c11atomics विकल्प माँगने के चरण पर।11
यदि Linux से कोड साझा करना आवश्यकता है, C11 थ्रेड (या pthread रैपर) का वास्तविक मूल्य है, पर केवल-Windows कोडबेस के लिए इस लेख का Win32 तरीका उपलब्ध जानकारी, ट्रैक रिकॉर्ड और डीबग आसानी में आगे है। जो भी चुनें, अब तक के डिज़ाइन सिद्धांत — साझा घटाना, लॉक और डेटा की संगतता, सहकारी शटडाउन — नहीं बदलते।
9. सत्यापन और डिबग — «पुनरुत्पादित नहीं होता» की तैयारी
रेस बग पकड़ने की परीक्षण से अपेक्षा नहीं कर सकते। साधारण परीक्षण उस रन को पास गिनते हैं जहाँ रेस «बस नहीं चली»। बचाव तीन परतों में सोचें।
पहली पंक्ति डिज़ाइन है। समीक्षा में तालिका से पुष्टि करें: कौन सा परिवर्तनीय डेटा साझा है, कौन सा लॉक हर टुकड़े की रक्षा करता है (खंड 5.1 की संगतता), लॉक अधिग्रहण क्रम अस्पष्ट तो नहीं, और स्टॉप इवेंट हर वर्कर तक पहुँचता है या नहीं। जो डिज़ाइन यह तालिका न भर सके अभी अधूरा है, भले अभी चलता हो।
दूसरा, विसंगतियाँ प्रेक्षणीय बनाएँ। INFINITE से बिना शर्त प्रतीक्षा करने के बजाय मुख्य बिंदुओं पर टाइमआउट लगाएँ और टाइमआउट चलने पर लॉग करें, हमेशा चलने वाले हैंग को पता चलने योग्य विफलता में बदलते हुए। मैदान में हैंग हो तो डंप लें, हर थ्रेड का स्टैक जाँचें, और चक्र खोजें कि कौन किसके लॉक की प्रतीक्षा कर रहा है। DLL के आसपास त्रुटियों के लिए Application Verifier से जाँच आधिकारिक रूप से अनुशंसित है।10 डंप और लॉग बनाना “Designing Windows Apps to Leave Logs and Dumps When They Crash” में है।
तीसरा, भार से हिलाएँ। तनाव परीक्षण — कोर से अधिक थ्रेड चलाकर, प्रसंस्करण क्रम यादृच्छिक कर, कृत्रिम विलंब डालकर — विकास मशीन पर रेस «जैकपॉट» लगने की संभावना बढ़ाने का व्यावहारिक तरीका है। अनुकूलित रिलीज़ बिल्ड पर भारी भार में पुनरुत्पादन भी न भूलें।
10. सारांश — C संस्करण जाँचसूची
- क्या हर थ्रेड
_beginthreadexसे बना है (CreateThread/_beginthreadमिले नहीं)? - क्या
CloseHandleसे पहले थ्रेड हैंडल जॉइन (WaitForSingleObject) करते हैं? - क्या अल्पकालिक कामों के लिए अपने थ्रेड ढेर बना रहे हैं (क्या थ्रेड पूल API को सौंपे जा सकते हैं)?
- क्या प्रक्रिया-आंतरिक बहिष्करण SRW लॉक / CRITICAL_SECTION इस्तेमाल करता है (Mutex का दुरुपयोग नहीं)?
- क्या साझा काउंटर और फ़्लैग
volatileपर भरोसा किए बिना Interlocked से अपडेट होते हैं? - क्या कोई
Sleepपोलिंग बचा है (क्या कंडीशन वेरिएबल या इवेंट प्रतीक्षा से बदला)? - क्या
TerminateThread(दूसरे थ्रेड की जबरन हत्या) हर जगह अनुपस्थित है? क्या वर्करExitThreadबुलाने के बजाय थ्रेड फ़ंक्शन सेreturnसे खत्म होते हैं (ताकि CRT सफ़ाई_endthreadexसे सही चले)? - क्या हर वर्कर के पास स्टॉप इवेंट प्लस
WaitForMultipleObjectsका स्टॉप पथ है, और क्या I/O पर अवरुद्ध थ्रेड भी जगा सकते हैं? - क्या हर वापसी पथ पर लॉक और हैंडल रिहाई गारंटी है (
goto cleanupअनुशासन)? - क्या
DllMainथ्रेड बनाने, सिंक्रनाइज़ करने या थ्रेड खत्म होने की प्रतीक्षा से बचता है?
भाषा से मदद न मिलने के बदले, C में मल्टीथ्रेडिंग की गुणवत्ता ठीक वही है जो आपके API चुनाव और अनुशासन बनाते हैं। _beginthreadex, SRW लॉक, Interlocked फ़ंक्शन और स्टॉप इवेंट को अपना डिफ़ॉल्ट चार-टुकड़ा सेट बनाएँ, और C में भी «कभी-कभी अटक जाती है» से दूर डिज़ाइन कर सकते हैं।
संबंधित लेख
- व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम अभ्यास: .NET संस्करण — और थ्रेड जोड़ने से पहले क्या तय करें
- व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम अभ्यास: C++ संस्करण — RAII और jthread से संरचना द्वारा दुर्घटनाएँ हटाना
- व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम अभ्यास: Java संस्करण — वर्चुअल थ्रेड युग की परिपाटियाँ
- Shared Memory Pitfalls and Practical Best Practices
- Why You Should Prefer Event Waits over Sleep(1) on Windows
- Serial Communication App Pitfalls - Through Reconnection and Log Design
- Designing Windows Apps to Leave Logs and Dumps When They Crash
संबंधित परामर्श क्षेत्र
KomuraSoft LLC C में लिखी निवासी प्रक्रियाओं, उपकरण-नियंत्रण ऐप और DLL की मल्टीथ्रेडिंग डिज़ाइन समीक्षा; TerminateThread या लीक लॉक से हुए हैंग और क्रैश की मूल-कारण जाँच (डंप विश्लेषण); और विरासत C कोड में थ्रेड जोड़ने पर तकनीकी परामर्श सँभालता है।
संदर्भ लिंक
-
Microsoft Learn, CreateThread function. इस पर कि CRT बुलाने वाले एक्ज़ीक्यूटेबल के भीतर थ्रेड CreateThread / ExitThread की जगह _beginthreadex / _endthreadex से प्रबंधित हों, और कि CreateThread से बना थ्रेड CRT बुलाए तो कम-मेमोरी पर CRT प्रक्रिया समाप्त कर सकता है। ↩ ↩2
-
Microsoft Learn, Multithreading with C and Win32. इस पर कि CRT लाइब्रेरी बुलाने वाले प्रोग्राम थ्रेड _beginthread / _beginthreadex से शुरू करें न कि Win32 CreateThread / ExitThread से; कि _beginthread परिवार CRT के प्रति-थ्रेड वेरिएबल आरंभ करता है; और कि SuspendThread CRT आंतरिक डेटा संरचनाओं तक पहुँचते थ्रेड को रोक सकता है, जिससे डेडलॉक हो सकता है। ↩ ↩2
-
Microsoft Learn, About Synchronization. Win32 सिंक्रनाइज़ेशन प्रिमिटिव चुनने के मार्गदर्शन पर: नए कोड के लिए डिफ़ॉल्ट SRW लॉक, पॉइंटर आकार और आमतौर पर यूज़र मोड में; पुनरावर्ती अधिग्रहण चाहिए तो CRITICAL_SECTION; Mutex हमेशा कर्नेल ऑब्जेक्ट, नामित प्रक्रिया-पार सिंक्रनाइज़ेशन और WaitForMultipleObjects के साथ; प्रक्रिया-आंतरिक सिंक्रनाइज़ेशन के लिए Mutex बार-बार संक्रियाओं में कहीं धीमी «सामान्य गलती»; सेमाफोर संसाधन पूल समवर्ती पहुँच सीमित करने, इवेंट सूचना के लिए। ↩ ↩2 ↩3
-
Microsoft Learn, Interlocked Variable Access. इस पर कि Interlocked फ़ंक्शन कई थ्रेडों में साझा वेरिएबल की पहुँच सिंक्रनाइज़ कर संक्रिया अविभाज्य करते हैं; कि InterlockedIncrement / Decrement पढ़ना, जोड़ना और वापस लिखना एक अणु संक्रिया में बाँधते हैं, क्योंकि बिना सिंक्रनाइज़ेशन दो थ्रेडों की एक साथ वृद्धि एक वृद्धि खो सकती है; InterlockedExchange / InterlockedCompareExchange परिवार पर; साझा मेमोरी में वेरिएबल हो तो अलग प्रक्रियाओं के थ्रेडों के बीच प्रयोग पर; और कि अधिकतर Interlocked पूर्ण मेमोरी बैरियर देते हैं, Acquire / Release रूप क्रम अर्थ चुनने को उपलब्ध। ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using Condition Variables. CRITICAL_SECTION से सुरक्षित सीमाबद्ध वृत्ताकार बफ़र पर निर्माता/उपभोक्ता कतार के कार्यित उदाहरण पर; संरचना पर जहाँ InitializeConditionVariable कंडीशन वेरिएबल बनाता है, उपभोक्ता SleepConditionVariableCS से प्रतीक्षा करता है, निर्माता WakeConditionVariable से जगाता है; और कि कंडीशन वेरिएबल Windows Vista से समर्थित हैं। ↩ ↩2 ↩3
-
Microsoft Learn, TerminateThread function. इस पर कि TerminateThread लक्ष्य थ्रेड को कोई यूज़र-मोड कोड चलाने दिए बिना खत्म करता है; कि लक्ष्य का क्रिटिकल सेक्शन पकड़े होने पर नहीं छूटता; कि हीप से मेमोरी आवंटित करते हीप लॉक नहीं छूटता; कि kernel32 की स्थिति या DLL की ग्लोबल स्थिति भ्रष्ट हो सकती है; और कि यह «एक खतरनाक फ़ंक्शन है जिसका उपयोग केवल सबसे चरम मामलों में होना चाहिए», जिसे न बुलाएँ जब तक लक्ष्य थ्रेड जो कोड पथ चला सकता हो उसे पूरी तरह जानते और नियंत्रित करते हों। ↩ ↩2
-
Microsoft Learn, Warning C6258. इस पर कि कोड-विश्लेषण चेतावनी C6258 TerminateThread का उपयोग पकड़ती है; कि TerminateThread उचित थ्रेड सफ़ाई नहीं कर सकता; और सही समाप्ति प्रक्रिया CreateEvent से इवेंट बनाना, हर थ्रेड WaitForSingleObject से इवेंट स्थिति देखना, और इवेंट संकेतित होते थ्रेड का स्वयं निष्पादन खत्म करना दिखाई गई है। ↩ ↩2 ↩3
-
Microsoft Learn, CreateThreadpoolWork function. इस पर कि CreateThreadpoolWork से वर्क ऑब्जेक्ट बनाएँ और हर SubmitThreadpoolWork पर पूल वर्कर थ्रेड कॉलबैक चलाए; कि कॉलबैक वातावरण (TP_CALLBACK_ENVIRON) से निष्पादन वातावरण निर्दिष्ट हो सकता है; और कि Windows Vista से उपलब्ध है। ↩ ↩2
-
Microsoft Learn, Thread Pools. इस पर कि थ्रेड पूल छोटे असिंक्रोनस कामों की बड़ी संख्या चलाने वाले, या अक्सर अल्पकालिक थ्रेड बनाने वाले ऐप के अनुकूल है; Vista में पुनः डिज़ाइन नए थ्रेड पूल API के घटकों पर; सर्वोत्तम अभ्यासों पर कि पूल थ्रेड TerminateThread से न खत्म करें न कॉलबैक से ExitThread बुलाएँ, कॉलबैक में बनी स्थिति लौटने से पहले साफ़ करें, और प्रतीक्षा हैंडल पूल के इस्तेमाल खत्म होने तक जीवित रखें। ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices. इस पर कि DllMain लोडर लॉक पकड़े बुलाई जाती है, जो सुरक्षित बुलाने योग्य API पर गंभीर सीमाएँ डालती है; कि DllMain में अन्य थ्रेडों से सिंक्रनाइज़ करना डेडलॉक लाता है; कि LoadLibrary बुलाना निषिद्ध कार्यों की सूची में है; कि DLL अनलोड पर DllMain में थ्रेड खत्म होने की प्रतीक्षा उस थ्रेड के DLL_THREAD_DETACH पहुँचाने लोडर लॉक लेने के प्रयास से डेडलॉक होती है; कि आदर्श DllMain खाली स्टब के निकट है, आरंभीकरण जितना हो टालें; और लोडर लॉक शीर्ष पर लॉक पदानुक्रम परिभाषित करने पर। ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. C मानक लाइब्रेरी अनुरूपता तालिका पर, दिखाते हुए C11 थ्रेड (threads.h) Visual Studio 2022 17.8 से समर्थित; stdatomic.h प्रायोगिक ( /experimental:c11atomics विकल्प के पीछे); और C11 / C17 कंपाइलर समर्थन को Visual Studio 2019 16.8 या बाद के साथ मिलते Windows SDK की आवश्यकता। ↩ ↩2
-
Microsoft Learn, Interlocked Variable Access. इस पर कि सही संरेखित 32-बिट वेरिएबल का सादा पढ़ना या लिखना अणु है, पर पहुँच का सिंक्रनाइज़ेशन (क्रम) गारंटी नहीं; कि 64-बिट वेरिएबल का सादा पढ़ना-लिखना 64-बिट Windows पर अणु है पर 32-बिट Windows पर गारंटी नहीं; और कि अन्य आकार के वेरिएबल किसी प्लेटफ़ॉर्म पर अणु गारंटी नहीं। ↩ ↩2
-
Microsoft Learn, _beginthread, _beginthreadex. इस पर कि _beginthreadex _beginthread से सुरक्षित क्यों है: _beginthread से बना थ्रेड जल्दी खत्म हो तो लौटा हैंडल अमान्य (या दूसरे थ्रेड की ओर) रह सकता है; _beginthreadex का हैंडल कॉलर CloseHandle से बंद करे और उसकी वैधता गारंटी है; _beginthreadex हैंडल सिंक्रनाइज़ेशन API को देने देता है; थ्रेड फ़ंक्शन __stdcall कॉलिंग कन्वेंशन से थ्रेड निकास कोड लौटाता है; और मल्टीथ्रेडेड CRT से लिंक आवश्यक है। ↩
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम प्रथाएँ: C++ संस्करण — RAII और jthread से संरचना द्वारा दुर्घटनाएँ मिटाना
C++ में मल्टीथ्रेडिंग वह संसार है जहाँ डेटा रेस अपरिभाषित व्यवहार बन जाता है। यह लेख std::thread के डिस्ट्रक्टर का जाल, jthread और stop_t...
Win32 Thread Pool API — CreateThreadpoolWork से थ्रेड बनाए बिना समवर्तिता
क्या आपके नेटिव कोड में CreateThread कॉल बिखरी पड़ी हैं? यह लेख Vista में पुनर्डिज़ाइन की गई Win32 थ्रेड पूल API — work, timer, wait और i...
DllMain और Loader Lock — DLL आरंभीकरण में "कुछ न करें" कहे जाने का वास्तविक कारण
DllMain से LoadLibrary क्यों न बुलाएँ या दूसरे थ्रेड से सिंक्रोनाइज़ न करें। प्राथमिक स्रोतों से यह लेख समझाता है कि लोडर लॉक हर DLL सूचन...
"Not Responding" वास्तव में क्या है — Windows ऐप के हैंग का निर्णय कैसे करता है, और न हैंग करने वाले ऐप कैसे डिज़ाइन करें
Windows का "Not Responding" वह तंत्र है जिसमें OS तय करता है कि विंडो ने 5 सेकंड संदेश नहीं लिया और उसे घोस्ट विंडो से बदल देता है। यह ले...
स्प्यूरियस वेकअप — Condition Variable "बिना सूचना के" क्यों जागती है और Windows पर सही प्रतीक्षा कैसे करें
कंडीशन वेरिएबल की प्रतीक्षा सूचना आए बिना भी लौट सकती है (स्प्यूरियस वेकअप)। यह लेख Windows इम्प्लीमेंटेशन से समझाता है कि विनिर्देश इसे ...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
इस विषय से जुड़ी सेवाएँ
यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।
Windows ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- CreateThread इस्तेमाल करूँ या _beginthreadex?
- हर उस थ्रेड के लिए _beginthreadex इस्तेमाल करें जो C रनटाइम लाइब्रेरी (CRT) के फ़ंक्शन बुलाता है। _beginthreadex थ्रेड शुरू करने से पहले CRT को प्रति-थ्रेड चाहिए आंतरिक डेटा आरंभ करता है। आधिकारिक दस्तावेज़ साफ़ कहता है कि यदि CreateThread से बना थ्रेड CRT फ़ंक्शन बुलाए, तो कम मेमोरी पर CRT प्रक्रिया समाप्त कर सकता है। व्यवहार में C ऐप के थ्रेड लगभग हमेशा कहीं CRT फ़ंक्शन (printf, malloc, strtok आदि) बुलाते हैं, इसलिए नियम «हमेशा _beginthreadex» याद रखना हानिकारक नहीं। _beginthread (बिना ex) से भी बचें — उसमें जाल है कि बना थ्रेड जल्दी खत्म हो तो लौटा हैंडल अमान्य हो सकता है, इसलिए _beginthreadex चुनें, जिसका हैंडल सिंक्रनाइज़ेशन API को दिया जा सकता है।
- क्या मैं TerminateThread से थ्रेड नहीं रोक सकता?
- नहीं, नहीं करना चाहिए। TerminateThread लक्ष्य थ्रेड को कोई यूज़र-मोड कोड चलाने दिए बिना मिटा देता है, इसलिए यदि वह थ्रेड क्रिटिकल सेक्शन पकड़े था तो वह कभी नहीं छूटता, यदि हीप से मेमोरी आवंटित कर रहा था तो हीप लॉक पकड़ा रहता है, और यदि किसी DLL की ग्लोबल स्थिति बदल रहा था तो वह स्थिति भ्रष्ट हो जाती है। आधिकारिक दस्तावेज़ स्पष्ट कहता है कि यह «एक खतरनाक फ़ंक्शन है जिसका उपयोग केवल सबसे चरम मामलों में होना चाहिए», और कोड विश्लेषण इसे चेतावनी C6258 भी लगाता है। सही रोक सहकारी शटडाउन है: स्टॉप इवेंट बनाएँ, हर थ्रेड WaitForSingleObject / WaitForMultipleObjects से उस इवेंट पर नज़र रखे, और हर थ्रेड स्वयं साफ़ करके स्वयं खत्म हो।
- मैं प्रक्रिया के भीतर बहिष्करण के लिए Mutex इस्तेमाल कर रहा था। क्या गलत है?
- चलता है, पर प्रदर्शन का बड़ा दाम पड़ता है। Win32 Mutex हमेशा कर्नेल ऑब्जेक्ट है, इसलिए हर अधिग्रहण और रिहाई कर्नेल मोड में संक्रमण करती है। एक प्रक्रिया के भीतर बहिष्करण के लिए SRW लॉक या CRITICAL_SECTION — जो यूज़र मोड में रहते हैं और केवल प्रतिस्पर्धा पर कर्नेल प्रतीक्षा में गिरते हैं — कहीं तेज़ हैं, और आधिकारिक दस्तावेज़ प्रक्रिया-आंतरिक सिंक्रनाइज़ेशन के लिए Mutex को स्पष्ट रूप से «सामान्य गलती» कहता है। Mutex तब काम आता है जब नामित ऑब्जेक्ट के रूप में प्रक्रियाओं के आर-पार बहिष्करण चाहिए, या जब WaitForMultipleObjects से अन्य कर्नेल ऑब्जेक्ट के साथ उसकी प्रतीक्षा करनी हो।
- क्या Windows पर C11 के threads.h और stdatomic.h इस्तेमाल हो सकते हैं?
- MSVC में C11 थ्रेड (threads.h) Visual Studio 2022 17.8 से समर्थित हैं ( /std:c11 और मिलता Windows SDK चाहिए)। stdatomic.h अभी भी प्रायोगिक माना जाता है और /experimental:c11atomics विकल्प माँगता है (अगस्त 2026 की आधिकारिक अनुरूपता तालिका के अनुसार)। यदि पोर्टेबिलिटी सर्वोच्च प्राथमिकता है तो यह व्यवहार्य विकल्प है, पर केवल-Windows कोडबेस के लिए Win32 API (_beginthreadex, SRW लॉक, कंडीशन वेरिएबल, Interlocked फ़ंक्शन) लिखना उपलब्ध जानकारी और ट्रैक रिकॉर्ड के हिसाब से यथार्थवादी चुनाव है।
- क्या volatile जोड़ने से साझा फ़्लैग सुरक्षित हो जाता है?
- नहीं। C का volatile केवल कंपाइलर अनुकूलन जैसे रजिस्टर में मान कैश करना दबाता है — यह न तो ऑपरेशन की अणुता गारंटी देता है न प्रोसेसरों के आर-पार मेमोरी क्रम। सही संरेखित 32-बिट वेरिएबल का सादा पढ़ना या लिखना Windows पर स्वयं अणु है, पर «पढ़ो, जोड़ो और वापस लिखो» अलग चरणों में टूटता है, और आसपास की मेमोरी संक्रियाओं के सापेक्ष क्रम की भी गारंटी नहीं। साझा काउंटर या फ़्लैग अपडेट करने के लिए Interlocked परिवार इस्तेमाल करें। अधिकतर Interlocked फ़ंक्शन पूर्ण मेमोरी बैरियर लाते हैं, इसलिए क्रम की गारंटी भी साथ मिलती है। जब कई वेरिएबल एक साथ बचाने हों, SRW लॉक या CRITICAL_SECTION लें।