Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
· अद्यतन तिथि: · Go Komura · Windows, multithreading, C, Win32 API, business app, bug investigation, design
संशोधन इतिहास (पहला संस्करण, 2 Aug 2026 को प्रकाशित)
- पहला प्रकाशन
इस लेख का हवाला दें(DOI: 10.5281/zenodo.22175895)
यह लेख Zenodo पर संग्रहीत है। नीचे वह DOI है जो हमेशा नवीनतम संस्करण की ओर ले जाता है, और वह DOI भी जो आपके पढ़े जा रहे संस्करण से बँधा है।
Go Komura (2026). Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175895 https://comcomponent.com/hi/blog/multithreading-best-practices-c/
- DOI (नवीनतम संस्करण)
- 10.5281/zenodo.22175895
- DOI (यह संस्करण)
- 10.5281/zenodo.22175896
«मैं device control की resident process C में लिख रहा हूँ।» «बीस साल पुराने C app में thread जोड़ने पड़े।» «हम TerminateThread से thread रोकते हैं, पर कभी-कभी पूरी process अटक जाती है।» — C में multithreading वह दुनिया है जहाँ भाषा सबसे कम मदद देती है। न exceptions, न RAII, न templates; synchronization की correctness पूरी तरह इस पर टिकी है कि आप कौन से API चुनते हैं और उन्हें कितनी discipline से बुलाते हैं।
यह लेख practical multithreading श्रृंखला का C संस्करण है। Win32 API पर C लिखने वाले developers के लिए यह multithread design के सिद्धांत — threads का ढेर production बंद करें, shared mutable state घटाएँ, lock discipline रखें, और शुरू करने से पहले stop का design करें — Win32 के tools पर map करता है: thread कैसे बनाएँ (_beginthreadex), synchronization objects कैसे चुनें, TerminateThread के बिना stop पथ कैसे design करें, और DllMain की सीमाएँ, अगस्त 2026 तक के primary sources के इर्द-गिर्द संगठित। अकेले पढ़ने योग्य लिखा गया है। वही सिद्धांत, अन्य भाषाओं के लिए निकाले, .NET संस्करण, C++ संस्करण और Java संस्करण में भी हैं।
1. पहले निष्कर्ष
- Thread
_beginthreadexसे बनाएँ,CreateThreadसे नहीं। यदि CRT बुलाने वाला thread CreateThread से बना हो, कम memory पर CRT process terminate कर सकता है।12 - Process के भीतर default lock SRW lock है; CRITICAL_SECTION केवल जब recursive acquire चाहिए। Process के भीतर mutual exclusion के लिए Mutex «सामान्य गलती» है जो हमेशा kernel-mode transition लाती है।3
- एकल variable Interlocked परिवार से update करें।
volatileन atomicity guarantee देता है न ordering। अधिकतर Interlocked functions full memory barrier लाते हैं।4 - Wait condition variable (
SleepConditionVariableCSपरिवार) या event प्लस wait functions से करें।Sleepआधारित polling loop CPU और responsiveness दोनों बर्बाद करता है।5 TerminateThreadकभी न इस्तेमाल करें। यह खतरनाक function lock, heap और DLL state corrupt करता है, और code-analysis warning C6258 का लक्ष्य है। Stop को cooperative shutdown design करें: stop event प्लसWaitForMultipleObjects।67- छोटे काम Windows thread pool (
CreateThreadpoolWork) को सौंपें, अपने threads से parallel न करें। Pool threads कभीExitThread/TerminateThreadसे न खत्म करें।89 - DllMain में thread न बनाएँ, synchronize न करें, thread खत्म होने की wait न करें। इसे loader lock पकड़े बुलाया जाता है, इसलिए यह deadlock का अड्डा है।10
- C11 का
<threads.h>VS 2022 17.8 से प्रयोग योग्य है, पर<stdatomic.h>अभी experimental है। केवल-Windows codebase के लिए Win32 तरीका यथार्थवादी चुनाव है।11
2. Multithreading कठिन क्यों है? — race condition और deadlock
Multithreading जो समस्याएँ लाती है उन्हें भाषा निरपेक्ष उबालें तो दो तरह की बचती हैं।
Race condition वह bug है जिसमें परिणाम इस पर बदलता है कि कई threads किसी code टुकड़े तक किस क्रम में पहुँचते हैं। Classic उदाहरण shared counter है: एकल व्यंजक count++ machine-code स्तर पर तीन चरणों में टूटता है — पढ़ो, जोड़ो, वापस लिखो। यदि दो threads इन तीन चरणों में एक साथ प्रवेश करें, एक thread का जोड़ दूसरे के write-back पर overwrite होकर खो जाता है।4 परिणाम run से run बदलता है, और कौन सा परिणाम मिलेगा unpredictable है।
sequenceDiagram
participant A as Thread A
participant M as Shared variable count
participant B as Thread B
Note over M: count = 10
A->>M: पढ़ना (10)
B->>M: पढ़ना (10)
A->>A: Local जोड़ (11)
B->>B: Local जोड़ (11)
A->>M: वापस लिखना (11)
B->>M: वापस लिखना (11)
Note over M: दो increment के बावजूद count = 11<br/>Thread A की increment खो गई
चित्र 1: Classic race condition जिसमें shared counter एक increment खो देता है। यदि count++ के तीन चरणों में दूसरा thread बीच में आए, जो write-back बाद में होता है वही दूसरे को overwrite करता है
Deadlock वह state है जिसमें दो threads प्रत्येक उस lock की wait करते हैं जो दूसरा पकड़े है, और कोई आगे नहीं बढ़ सकता। Thread A lock 1 पकड़े lock 2 की wait करता है; Thread B lock 2 पकड़े lock 1 की wait करता है — इतना काफी है कि दोनों हमेशा के लिए रुक जाएँ।
flowchart LR
A["Thread A<br/>Lock 1 पकड़े"] -->|"Lock 2 की release की wait"| B["Thread B<br/>Lock 2 पकड़े"]
B -->|"Lock 1 की release की wait"| A
चित्र 2: Deadlock की circular wait। जिस क्षण wait तीर loop बनाते हैं, उस loop का हर thread हमेशा के लिए रुक जाता है
दोनों time-dependent हैं: execution-order combination जो development मशीन पर दसियों हज़ार runs में एक बार दिखे, अलग core count और timing वाली ग्राहक मशीन पर रोज़ हो सकता है। «Debugger लगा तो reproduce नहीं होता» इसलिए होता है कि observation स्वयं timing बदल देता है — race bug का विशिष्ट व्यवहार। इसलिए इस लेख का हर सिद्धांत एक दिशा में है: सही synchronize करने की चिंता से पहले वे जगहें घटाएँ जहाँ synchronization चाहिए।
2.1. C-विशिष्ट मान्यताएँ — भाषा आपको किसी चीज़ से नहीं बचाती
C में, क्योंकि भाषा के पास इन सिद्धांतों को लागू करने की व्यवस्था नहीं, उन्हें discipline के रूप में स्पष्ट लिखना पड़ता है।
पहला, release की guarantee structure में बनाएँ। C++ के RAII के समकक्ष कुछ न होने पर, lock release और handle पर CloseHandle को goto cleanup pattern से बचाना होता है जो हर function निकास एक जगह से निकालता है, या coding convention से जो हर acquire को release से जोड़ती है। जल्दी return जोड़कर lock leak करना C की classic accident है।
दूसरा, data race के साथ C++ जैसा व्यवहार करें। सही aligned 32-bit variable का सादा पढ़ना या लिखना Windows पर atomic है, पर उससे आगे — 32-bit Windows पर 64-bit variable, combined operations, या कई variables में consistency — कुछ भी guarantee नहीं।12 «संयोग से चलता» code compiler या optimization स्तर बदलते ही टूटता है।
तीसरा, ownership तय करें। Function comment में «यह buffer कौन सा thread लिखता है, और कब से किसका है» स्पष्ट करने की संस्कृति C multithreading में synchronization primitives के चुनाव जितनी ही फलती है।
3. Thread कैसे बनाएँ — _beginthreadex, और कुछ नहीं
3.1. CreateThread गलत चुनाव क्यों है
Win32 का native API CreateThread है, पर official guidance है कि कोई भी thread जो CRT (C runtime) functions बुलाए उसे _beginthreadex से बनाना चाहिए। _beginthreadex thread शुरू करने से पहले CRT को per-thread चाहिए internal data initialize करता है। यदि CreateThread से बना thread CRT functions बुलाए, कम-memory स्थिति में CRT process terminate कर सकता है।12 चूँकि printf, malloc और strtok सभी CRT functions हैं, व्यावहारिक नियम: «C में लिखे threads हमेशा _beginthreadex इस्तेमाल करें»।
_beginthread (बिना ex) से भी बचें। उसमें pitfall है: यदि बनाया thread जल्दी खत्म हो, लौटा handle पहले से invalid हो सकता है — या दूसरे thread की ओर इशारा भी कर सकता है — जबकि _beginthreadex, जिसका handle synchronization API को सुरक्षित दिया जा सकता है, सुरक्षित चुनाव है। Caller _beginthreadex का लौटा handle CloseHandle से बंद करता है।13
#include <process.h>
static unsigned __stdcall WorkerMain(void* arg)
{
WorkerContext* ctx = (WorkerContext*)arg;
/* ... खंड 5 और 6 का wait loop ... */
return 0;
}
HANDLE hThread = (HANDLE)_beginthreadex(
NULL, 0, WorkerMain, &ctx, 0, NULL);
if (hThread == NULL) { /* failure संभालना */ }
/* ...stop request के बाद... */
WaitForSingleObject(hThread, INFINITE); /* join */
CloseHandle(hThread);
3.2. छोटे काम Windows thread pool को जाएँ
यदि आप «बहुत से छोटे काम किसी चीज़ पर फेंकना» चाहते हैं या «बार-बार अल्पकालिक thread बना-मिटा रहे» हैं, अपने threads की जगह Windows thread pool (Vista से उपलब्ध thread pool API) इस्तेमाल करें। CreateThreadpoolWork से work object बनाएँ और SubmitThreadpoolWork से जमा करें, और pool के worker threads callbacks parallel चलाते हैं।8 Thread count का management OS के पास रहता है, और thread बनाने-मिटाने की लागत गायब हो जाती है। «अपने threads न बनाएँ» सिद्धांत का C उत्तर यही है।
Pool इस्तेमाल का discipline भी official रूप से दर्ज है: pool threads कभी TerminateThread / ExitThread से न खत्म करें; callback में बदली कोई state (TLS, thread priority आदि) लौटने से पहले बहाल करें; और wait handles pool के इस्तेमाल खत्म होने तक जीवित रखें।9 एक और व्यावहारिक note: pool केवल worker threads की संख्या सीमित करता है — SubmitThreadpoolWork से queued unexecuted callbacks बिना सीमा जमा हो सकते हैं। लंबी-चलती बनावट में जहाँ जमाव processing से आगे रहता है, app पक्ष पर semaphore जैसा admission limiter, या bounded queue रखें, ताकि जमा करने वाला पक्ष भर जाने पर wait करे या अस्वीकृत हो (यह backpressure है ताकि overload memory समस्या न बन जाए, और खंड 5 के queue design जैसा ही सिद्धांत)।
4. Shared mutable state न्यूनतम करें — partitioning, read-only, और hand-off
Race तभी होती है जब «कई threads» और «shared mutable data» दोनों मौजूद हों। Synchronization primitives चुनने (अगला अध्याय) से पहले सोचें कि sharing पहले ही घटाया जा सकता है या नहीं। तीन परिवार की तकनीकें हैं।
Partition करें। Parallel compilation में हर thread shared counter पर लिखने के बजाय per-thread local variable (या प्रति thread allocated buffer) में इस्तेमाल बनाएँ, और अंत में एक बार InterlockedAdd जैसी चीज़ से मिलाएँ। Shared state पर write «हर iteration» से «प्रति thread एक बार» गिरता है, और synchronization लागत व contention window दोनों परिमाण क्रम से सिकुड़ते हैं। खंड 2.1 का ownership discipline — «यह buffer किस thread का है» — partitioning का नक्शा बन जाता है।
Read-only बनाएँ। Startup पर बनी और बाद में न बदली गई configuration और तालिकाएँ initialization पूरा होने के बाद किसी भी संख्या के threads से पढ़ना सुरक्षित हैं। या तो सारे threads शुरू होने से पहले सारा initialization खत्म करें, या यदि lazy initialization चाहिए तो Win32 का one-time initialization (InitOnceExecuteOnce) इस्तेमाल करें, और सीमा — «कब से यह read-only हो जाता है» — code में स्पष्ट करें।3
Hand-off करें। दोनों पक्षों को shared variable छूने देने के बजाय, threads के बीच data flow producer/consumer queue से निकालें। C में implementation ठीक खंड 5 का condition-variable pattern है (bounded circular buffer प्लस SleepConditionVariableCS), जो official worked उदाहरण है; capacity limit वाला buffer स्वाभाविक backpressure भी देता है — production consumption से आगे निकलते ही wait करता है।5
5. Synchronization objects चुनना और lock discipline
Win32 में कई तरह के synchronization primitives हैं, और गलत चुनाव performance और correctness दोनों का दाम लेता है। Official guidance एक चित्र में संक्षेप।3
flowchart TB
S{"Processes के आर-पार<br/>synchronization चाहिए?"} -->|"हाँ"| Q2{"किस लिए?"}
Q2 -->|"Mutual exclusion"| MTX["Named Mutex"]
Q2 -->|"Concurrent access संख्या सीमित करना"| SEM["Named semaphore"]
Q2 -->|"Event notification"| EVT["Named event"]
S -->|"नहीं - process-internal"| Q3{"उसी thread से recursive<br/>acquire चाहिए?"}
Q3 -->|"हाँ"| CS["CRITICAL_SECTION"]
Q3 -->|"नहीं"| Q4{"Portable C++ code<br/>प्राथमिकता?"}
Q4 -->|"हाँ"| STD["std::mutex /<br/>std::shared_mutex"]
Q4 -->|"नहीं"| SRW["SRW lock - default चुनाव"]
चित्र 3: Win32 synchronization primitives कैसे चुनें। पहली शाखा «क्या processes को पार करता है» — मुद्दा यह है कि जब नहीं करता तो kernel object (Mutex) न उठाएँ
| Primitive | दायरा | विशेषताएँ | कहाँ इस्तेमाल करें |
|---|---|---|---|
| SRW lock | Process-internal | तेज़ (आमतौर पर पूरा user mode में रहता है), pointer आकार, non-recursive | नए code का default। AcquireSRWLockShared shared read access भी देता है |
| CRITICAL_SECTION | Process-internal | तेज़ (spin करता है, फिर kernel wait में गिरता है), recursive | जब उसी thread को recursive acquire चाहिए |
| Mutex | Process-internal / processes के आर-पार | हमेशा kernel object, इसलिए धीमा | Processes के आर-पार mutual exclusion (named), या WaitForMultipleObjects के साथ |
| Semaphore | Process-internal / processes के आर-पार | Kernel object | Resource pool पर concurrent access सीमित करना |
| Event | Process-internal / processes के आर-पार | Kernel object | सूचित करना कि «कुछ हुआ» (data बचाने के लिए नहीं) |
| Interlocked functions | Process-internal (shared memory पर processes के आर-पार भी) | Lock-free atomic operations | Counters, flags, pointer swap4 |
तालिका और flowchart की एक पादटिप्पणी। Event, semaphore और mutex जैसे kernel objects बिना नाम बनाए process-internal synchronization के लिए बिल्कुल काम करते हैं (खंड 6 का stop event ठीक एक unnamed event है)। Kernel object का अर्थ केवल processes के आर-पार नहीं। न ही, उलटा, «unnamed» सख्ती से «एक process तक सीमित» है — यदि आप child process को handle inherit दें, या DuplicateHandle से दूसरी process में copy करें, वही kernel object बिना नाम भी कई processes से इस्तेमाल हो सकता है। सटीक कहना यह है कि naming processes को वही object फिर खोलने देने का एक प्रतिनिधि तरीका है। चित्र 3 की शाखा चुनाव का मुद्दा «process-internal lock के लिए kernel object न चुनें» पकड़ती है — process-internal notification (event) या concurrency limit (semaphore) के लिए unnamed kernel object अभी भी सही उत्तर है।
Interlocked परिवार .NET संस्करण की Interlocked class और C++ संस्करण के std::atomic से मेल खाता है। InterlockedIncrement / InterlockedExchange / InterlockedCompareExchange एक variable पर operations indivisible करते हैं, और चूँकि इनमें से अधिकतर full memory barrier लाते हैं, ordering की guarantee भी मिलती है।4 «ठीक है क्योंकि मैंने volatile लगाया» भ्रम है — volatile न atomicity guarantee देता है न ordering (FAQ देखें)। एक और पूर्वापेक्षा: alignment। Interlocked function का target variable प्राकृतिक सीमा पर aligned होना चाहिए (32-bit value के लिए 4-byte सीमा, 64-bit के लिए 8-byte); नहीं तो व्यवहार unpredictable है।12 #pragma pack वाली struct के fields, या तार format पर सीधे map buffer के fields को Interlocked का target कभी न बनाएँ। Counters और flags साधारण घोषित variables तक सीमित रखें — जिन्हें compiler आपके लिए align करता है। Pointer swap जैसे InterlockedExchangePointer पर विशिष्ट चेतावनी भी है: indivisible केवल swap स्वयं है, और swap के बाद पुराने block के जीवन की guarantee कोई नहीं देता। यदि reader पुराना pointer load करे ठीक जब writer उसे बदलकर free बुलाए, वह मुक्त memory पर पहुँच है। Pointer swap से shared data update करने वाला design तभी चलता है जब reclamation protocol — lock, reference count या समान — के साथ जोड़ा हो (संदेह हो तो SRW lock से बचाना सुरक्षित default है)।
किसी चीज़ की wait के लिए condition variables हैं। InitializeConditionVariable से एक बनाएँ; consumer SleepConditionVariableCS पर सोता है (CRITICAL_SECTION के साथ जोड़ा), producer WakeConditionVariable से जगाता है — यही bounded buffer पर official producer/consumer queue उदाहरण का आकार है।5 महत्वपूर्ण discipline: जागने पर, lock के भीतर शर्त (क्या queue खाली नहीं) हमेशा फिर जाँचें, और झूठ हो तो wait पर लौटें। Condition variables बिना किसी सूचना के spurious wakeup झेल सकते हैं, और जब आप जागें तब तक दूसरा consumer item पहले ले चुका हो — इसलिए «मुझे जगाया गया» जरूरी नहीं «शर्त सत्य है»। यह .NET संस्करण खंड 4.3 के channel और C++ संस्करण खंड 4 की BlockingQueue जैसा आकार C में बनाने का औज़ार है। SRW lock से जोड़ते समय SleepConditionVariableSRW इस्तेमाल करें।
5.1. Lock discipline — तीन सिद्धांत जो कोई भी primitive चुनें टिकते हैं
सही primitive चुनना अकेले काफी नहीं — disciplined इस्तेमाल के बिना race फिर भी नहीं रुकेंगी।
- One-to-one तय करें कौन सा lock कौन सा data बचाता है। हर उस mutable data समूह को ठीक एक lock (SRW lock या CRITICAL_SECTION) दें जिसे बचाना चाहते हैं, और उस data को छूने वाली हर जगह वही lock लें। C में खासकर header comment में स्पष्ट लिखना फलता है — «इस struct की रक्षा
g_lockFooकरता है»। - Lock पकड़े कुछ धीमा या बाहरी न करें। Lock पकड़े केवल वही ठीक है जो वह data पढ़ना या लिखना जिसे वह बचाता है। Lock अभी पकड़े file I/O, network call या callback invocation पकड़ने का समय बढ़ाते हैं और जोखिम है कि बुलाया गया दूसरा lock लेने की कोशिश करे, चित्र 2 की circular wait बनाता हुआ।
- कई locks का acquire क्रम स्थिर करें। जहाँ दो या अधिक lock लिए जाएँ, नियम बनाएँ कि हर thread उन्हें उसी क्रम में ले (lock hierarchy)। DLL best-practices documents स्पष्ट कहते हैं कि उस क्रम को पलटना (lock order inversion) ऐसे deadlock पैदा करता है जिन्हें debug करना कठिन है, और hierarchy परिभाषित कर लगातार उसका पालन करना चाहिए।10
6. Stop का design — कभी TerminateThread नहीं
6.1. TerminateThread क्या तोड़ता है
TerminateThread target thread को कोई user-mode code चलाने दिए बिना मिटा देता है। Official documents जो परिणाम गिनाते हैं वे गंभीर हैं। यदि target thread critical section पकड़े था, वह कभी नहीं छूटता; यदि heap operation के बीच था, heap lock पकड़ा रहता है (और बाद का हर thread जो malloc बुलाए अटकता है); और यदि किसी DLL की global state बदल रहा था, वह state corrupt रह जाती है। Official स्थिति है कि यह «एक खतरनाक function है जिसका इस्तेमाल केवल सबसे चरम मामलों में होना चाहिए», और code analysis इसे warning C6258 लगाता है।67
«कभी-कभी पूरी तरह अटकने» वाले app की जाँच में TerminateThread मिलना व्यवहार में सचमुच आम दृश्य है। मिले तो उसे ठीक करने योग्य समझें।
6.2. सही pattern: Stop event प्लस WaitForMultipleObjects
C में cooperative shutdown का स्थापित pattern एक manual-reset stop event बनाना है, और हर worker thread को «work संकेत» और «stop संकेत» एक साथ wait कराना। C6258 warning documents स्वयं ठीक इसी pattern की ओर इशारा करते हैं — event बनाएँ, हर thread WaitForSingleObject से उसकी निगरानी करे, और thread स्वयं खत्म हो — सही समाप्ति के रूप में।7
HANDLE hStopEvent; /* CreateEvent(NULL, TRUE, FALSE, NULL): manual-reset */
HANDLE hWorkEvent; /* CreateEvent(NULL, FALSE, FALSE, NULL): auto-reset.
प्राप्त होते ही स्वतः non-signaled पर लौटता है
(manual-reset event एक बार संकेत के बाद
waits सीधी पार कर देता, खाली queue पर
घूमता busy loop बन जाता) */
static unsigned __stdcall WorkerMain(void* arg)
{
HANDLE waits[2] = { hStopEvent, hWorkEvent };
for (;;) {
DWORD r = WaitForMultipleObjects(2, waits, FALSE, INFINITE);
if (r == WAIT_FAILED) { /* जैसे invalid handle. बिना संभाले यह खाली घूमता है */
LogLastError(); /* GetLastError() लिखें और निकलें */
break;
}
if (r == WAIT_OBJECT_0) /* Stop request */
break;
if (r == WAIT_OBJECT_0 + 1) { /* काम उपलब्ध */
/* ProcessNextItem को भी stop event दें: यदि वह एक item के लिए
भीतर देर wait करे, और वहाँ भी stop न देख सके,
shutdown उस एक item का बंधक हो जाता है */
while (ProcessNextItem(hStopEvent)) { /* Queue से एक item process; खाली हो तो FALSE */
/* निकालते समय भी stop request जाँचें. इसे छोड़ें तो काम
जमा रहता है तो रुक नहीं सकते (stop starvation) */
if (WaitForSingleObject(hStopEvent, 0) == WAIT_OBJECT_0)
break;
}
}
}
Cleanup(); /* अपनी cleanup स्वयं करें */
return 0; /* स्वयं खत्म हों */
}
BOOL StopWorkers(HANDLE* threads, DWORD count)
{
BOOL ok = TRUE;
if (!SetEvent(hStopEvent)) { /* यदि stop request नहीं पहुँचा, */
LogLastError(); /* unlimited join पर न जाएँ */
return FALSE;
}
/* अब सबके पास stop request एक साथ उठा है */
for (DWORD i = 0; i < count; i++) {
if (WaitForSingleObject(threads[i], INFINITE) == WAIT_OBJECT_0) {
CloseHandle(threads[i]); /* केवल वे handles बंद करें जिनका join पुष्टि हुआ */
} else {
LogLastError(); /* WAIT_FAILED: जैसे invalid handle */
ok = FALSE; /* «सब रुके» न बताएँ */
}
}
return ok; /* FALSE हो तो shared resources छोड़ने पर न बढ़ें */
}
रोकने वाला पक्ष WaitForSingleObject से एक-एक thread join करे, इसका कारण है। WaitForMultipleObjects एक साथ अधिकतम MAXIMUM_WAIT_OBJECTS (64) handles की wait कर सकता है; बड़ा array दें तो wait स्वयं WAIT_FAILED से असफल हो जाती है, और आप handles बंद करते रह जाते हैं यह मानकर कि सबकी wait की जबकि किसी की नहीं की। यदि केवल सबके खत्म होने की wait चाहिए, बिना ऊपरी सीमा के एक-एक loop सुरक्षित चुनाव है।
flowchart TB
OWNER["रोकने वाला"] -->|"SetEvent(hStopEvent)"| SE["Stop event<br/>manual-reset - सबको एक साथ दिखे"]
SE --> W1["Worker 1 - stop और काम की<br/>एक साथ wait WaitForMultipleObjects से"]
SE --> W2["Worker 2 - stop और काम की<br/>एक साथ wait WaitForMultipleObjects से"]
W1 --> C1["साफ़ करके स्वयं लौटता है"]
W2 --> C2["साफ़ करके स्वयं लौटता है"]
C1 --> J["रोकने वाला thread handle की wait कर join करता है<br/>अब ही रुका कह सकते हैं"]
C2 --> J
चित्र 4: Stop-event pattern। Stop संकेत के लिए manual-reset event का अर्थ है एक SetEvent हर wait करता worker को एक साथ जगाता है। हर thread स्वयं तय करता है कैसे खत्म हो, और stop तभी पूर्ण मानी जाती है जब join पूरा हो
तीन मुख्य बिंदु हैं। Stop event manual-reset बनाएँ (ताकि एक SetEvent हर worker को दिखे); wait array में stop event पहले रखें (ताकि दोनों एक साथ signaled हों तो stop को प्राथमिकता मिले); और रोकने वाला हमेशा handle बंद करने से पहले thread handle join करे।
दायरे की दो चेतावनियाँ। पहला, यह «event प्लस सब-निकालो» pattern एकल-worker बनावट के लिए है। Auto-reset event पर कितनी भी बार SetEvent बुलाएँ, वह केवल «एक signaled state है» व्यक्त कर सकता है (लगातार संकेत मिल जाते हैं), इसलिए कई workers हों तो एक जागता है और पूरा burst क्रमिक process करता है। यदि कई workers queue share करें, काम संकेत semaphore पर बदलें, हर item queue में रखते ReleaseSemaphore(hSem, 1, NULL) से count बढ़ाएँ। सफल semaphore wait एक count खाती है, सही correspondence देती हुई: queued items जितने wait करते workers एक-एक जागते हैं (यह इस्तेमाल semaphore के कार्यक्षेत्र में है, चित्र 3 तालिका के «resource pool पर concurrent access सीमित करना» जैसा)। पर semaphore पर जाते समय consumer पक्ष भी बदलें ताकि एक सफल wait ठीक एक item process करने के बराबर हो। ऊपर के sample का सब-निकालो loop ज्यों का त्यों छोड़ें, और एक wait — जो केवल एक permit खाती है — पूरी queue खाली कर देगी, हिसाब बिगाड़ते हुए: बची permits पर अन्य workers खाली queue पर जागते हैं, और producer का ReleaseSemaphore अधिकतम count पार करने से असफल होने लगता है। «एक permit बराबर एक काम» correspondence रखना semaphore दृष्टिकोण की पूर्वापेक्षा है। दूसरा, stop पथ एक item के processing से भी निकालें। यदि ProcessNextItem भीतर लंबी blocking wait करे, या वहाँ भी stop event दें और दोनों की साथ wait करें, या finite timeout लगाएँ। केवल items के बीच जाँच «एक item न खत्म होने से shutdown हमेशा wait करे» का छेद छोड़ती है। यह वही बात कहता है जो .NET संस्करण का StopAsync और C++ संस्करण का jthread प्लस join।
Blocking I/O (pipe, socket, serial port) पर wait करता thread event जाँचने नहीं लौट सकता, इसलिए I/O पक्ष को अपना design चाहिए — या OVERLAPPED I/O event के साथ जिसे साथ wait करें, या CancelIoEx से I/O जगाएँ (ठोस serial उदाहरण के लिए “Serial Communication App Pitfalls - Through Reconnection and Log Design” देखें)।
7. DllMain और loader lock — DLL लेखकों के लिए बारूदी सुरंग
C में लिखे shared components अक्सर DLL बन जाते हैं, और DLL की अपनी बाधा है: loader lock। OS loader DllMain को loader lock पकड़े बुलाता है, इसलिए इसमें निम्न में से कुछ भी करना deadlock या crash का स्रोत बनता है।10
- अन्य threads से synchronize करना (lock लेना, thread खत्म होने की wait)
LoadLibrary/FreeLibraryबुलाना, सीधे या अप्रत्यक्ष- Thread बनाना (synchronization हो तो खतरनाक), या
ExitThreadबुलाना
«DLL unload होते worker thread खत्म होने की DllMain में wait» बिल्कुल उचित दिखती है पर classic deadlock है: खत्म होता thread DLL_THREAD_DETACH पहुँचाने loader lock लेने की कोशिश करता है, और दोनों पक्ष एक-दूसरे की wait करते रह जाते हैं। अपने threads वाली DLL को स्पष्ट initialization और shutdown functions — जैसे MyLib_Init / MyLib_Shutdown — उजागर करने चाहिए, और thread start व join वहीं करने चाहिए। आदर्श DllMain खाली stub के निकट है।10
8. C11 thread विकल्प — स्थिति कहाँ है
यदि Win32 पर depend न रहने वाला portable C लिखना हो, विकल्प C11 का <threads.h> (thrd_create / mtx_lock / cnd_wait) और <stdatomic.h> है। Official conformance तालिका के अनुसार MSVC support यह है: <threads.h> Visual Studio 2022 17.8 से supported (/std:c11 और मिलता Windows SDK चाहिए), जबकि <stdatomic.h> अभी experimental है, /experimental:c11atomics option माँगने के चरण पर।11
यदि Linux से code share करना requirement है, C11 threads (या pthread wrapper) का वास्तविक मूल्य है, पर केवल-Windows codebase के लिए इस लेख का Win32 तरीका उपलब्ध जानकारी, track record और debug आसानी में आगे है। जो भी चुनें, अब तक के design सिद्धांत — sharing घटाना, lock और data की correspondence, cooperative shutdown — नहीं बदलते।
9. Verification और debug — «reproduce नहीं होता» की तैयारी
Race bugs पकड़ने की testing से अपेक्षा नहीं कर सकते। साधारण tests उस run को pass गिनते हैं जहाँ race «बस नहीं चली»। बचाव तीन परतों में सोचें।
पहली पंक्ति design है। Review में तालिका से पुष्टि करें: कौन सा mutable data shared है, कौन सा lock हर टुकड़े की रक्षा करता है (खंड 5.1 की correspondence), lock acquire क्रम अस्पष्ट तो नहीं, और stop event हर worker तक पहुँचता है या नहीं। जो design यह तालिका न भर सके अभी अधूरा है, भले अभी चलता हो।
दूसरा, anomalies observable बनाएँ। INFINITE से बिना शर्त wait करने के बजाय मुख्य बिंदुओं पर timeout लगाएँ और timeout चलने पर log करें, हमेशा चलने वाले hang को पता चलने योग्य failure में बदलते हुए। मैदान में hang हो तो dump लें, हर thread का stack जाँचें, और cycle खोजें कि कौन किसके lock की wait कर रहा है। DLL के आसपास errors के लिए Application Verifier से जाँच official रूप से recommended है।10 Dump और log बनाना “Designing Windows Apps to Leave Logs and Dumps When They Crash” में है।
तीसरा, load से हिलाएँ। Stress testing — cores से अधिक threads चलाकर, processing क्रम random कर, कृत्रिम delay डालकर — development मशीन पर race «jackpot» लगने की संभावना बढ़ाने का व्यावहारिक तरीका है। Optimized release build पर भारी load में reproduce भी न भूलें।
10. सारांश — C संस्करण checklist
- क्या हर thread
_beginthreadexसे बना है (CreateThread/_beginthreadमिले नहीं)? - क्या
CloseHandleसे पहले thread handle join (WaitForSingleObject) करते हैं? - क्या अल्पकालिक कामों के लिए अपने threads ढेर बना रहे हैं (क्या thread pool API को सौंपे जा सकते हैं)?
- क्या process-internal mutual exclusion SRW lock / CRITICAL_SECTION इस्तेमाल करता है (Mutex का दुरुपयोग नहीं)?
- क्या shared counters और flags
volatileपर भरोसा किए बिना Interlocked से update होते हैं? - क्या कोई
Sleeppolling बचा है (क्या condition variable या event wait से बदला)? - क्या
TerminateThread(दूसरे thread की जबरन हत्या) हर जगह अनुपस्थित है? क्या workersExitThreadबुलाने के बजाय thread function सेreturnसे खत्म होते हैं (ताकि CRT cleanup_endthreadexसे सही चले)? - क्या हर worker के पास stop event प्लस
WaitForMultipleObjectsका stop पथ है, और क्या I/O पर blocked threads भी जगा सकते हैं? - क्या हर return पथ पर lock और handle release guarantee है (
goto cleanupdiscipline)? - क्या
DllMainthread बनाने, synchronize करने या thread खत्म होने की wait से बचता है?
भाषा से मदद न मिलने के बदले, C में multithreading की गुणवत्ता ठीक वही है जो आपके API चुनाव और discipline बनाते हैं। _beginthreadex, SRW lock, Interlocked functions और stop event को अपना default चार-टुकड़ा सेट बनाएँ, और C में भी «कभी-कभी अटक जाती है» से दूर design कर सकते हैं।
संबंधित लेख
- Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
- Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents हटाना
- Practical multithreading best practices: Java संस्करण — virtual thread युग की practices
- 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 में लिखी resident processes, device-control apps और DLLs की multithreading design review; TerminateThread या leaked lock से हुए hang और crash की root-cause investigation (dump analysis); और legacy C code में thread जोड़ने पर technical consulting सँभालता है।
संदर्भ लिंक
-
Microsoft Learn, CreateThread function. इस पर कि CRT बुलाने वाले executable के भीतर threads CreateThread / ExitThread की जगह _beginthreadex / _endthreadex से manage हों, और कि CreateThread से बना thread CRT बुलाए तो कम-memory पर CRT process terminate कर सकता है। ↩ ↩2
-
Microsoft Learn, Multithreading with C and Win32. इस पर कि CRT library बुलाने वाले program threads _beginthread / _beginthreadex से शुरू करें न कि Win32 CreateThread / ExitThread से; कि _beginthread परिवार CRT के per-thread variables initialize करता है; और कि SuspendThread CRT internal data structures तक पहुँचते thread को रोक सकता है, जिससे deadlock हो सकता है। ↩ ↩2
-
Microsoft Learn, About Synchronization. Win32 synchronization primitives चुनने के guidance पर: नए code के लिए default SRW lock, pointer आकार और आमतौर पर user mode में; recursive acquire चाहिए तो CRITICAL_SECTION; Mutex हमेशा kernel object, named process-पार synchronization और WaitForMultipleObjects के साथ; process-internal synchronization के लिए Mutex बार-बार operations में कहीं धीमी «सामान्य गलती»; semaphore resource pool concurrent access सीमित करने, event notification के लिए। ↩ ↩2 ↩3
-
Microsoft Learn, Interlocked Variable Access. इस पर कि Interlocked functions कई threads में shared variable की पहुँच synchronize कर operations indivisible करते हैं; कि InterlockedIncrement / Decrement पढ़ना, जोड़ना और वापस लिखना एक atomic operation में बाँधते हैं, क्योंकि बिना synchronization दो threads की एक साथ increment एक increment खो सकती है; InterlockedExchange / InterlockedCompareExchange परिवार पर; shared memory में variable हो तो अलग processes के threads के बीच प्रयोग पर; और कि अधिकतर Interlocked full memory barrier देते हैं, Acquire / Release रूप ordering semantics चुनने को उपलब्ध। ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Using Condition Variables. CRITICAL_SECTION से सुरक्षित bounded circular buffer पर producer/consumer queue के worked उदाहरण पर; संरचना पर जहाँ InitializeConditionVariable condition variable बनाता है, consumer SleepConditionVariableCS से wait करता है, producer WakeConditionVariable से जगाता है; और कि condition variables Windows Vista से supported हैं। ↩ ↩2 ↩3
-
Microsoft Learn, TerminateThread function. इस पर कि TerminateThread target thread को कोई user-mode code चलाने दिए बिना खत्म करता है; कि target का critical section पकड़े होने पर नहीं छूटता; कि heap से memory allocate करते heap lock नहीं छूटता; कि kernel32 की state या DLL की global state corrupt हो सकती है; और कि यह «एक खतरनाक function है जिसका इस्तेमाल केवल सबसे चरम मामलों में होना चाहिए», जिसे न बुलाएँ जब तक target thread जो code पथ चला सकता हो उसे पूरी तरह जानते और नियंत्रित करते हों। ↩ ↩2
-
Microsoft Learn, Warning C6258. इस पर कि code-analysis warning C6258 TerminateThread का इस्तेमाल पकड़ती है; कि TerminateThread उचित thread cleanup नहीं कर सकता; और सही समाप्ति प्रक्रिया CreateEvent से event बनाना, हर thread WaitForSingleObject से event state देखना, और event signaled होते thread का स्वयं execution खत्म करना दिखाई गई है। ↩ ↩2 ↩3
-
Microsoft Learn, CreateThreadpoolWork function. इस पर कि CreateThreadpoolWork से work object बनाएँ और हर SubmitThreadpoolWork पर pool worker thread callback चलाए; कि callback environment (TP_CALLBACK_ENVIRON) से execution environment निर्दिष्ट हो सकता है; और कि Windows Vista से उपलब्ध है। ↩ ↩2
-
Microsoft Learn, Thread Pools. इस पर कि thread pool छोटे asynchronous कामों की बड़ी संख्या चलाने वाले, या अक्सर अल्पकालिक threads बनाने वाले app के अनुकूल है; Vista में पुनः design नए thread pool API के components पर; best practices पर कि pool threads TerminateThread से न खत्म करें न callback से ExitThread बुलाएँ, callback में बनी state लौटने से पहले साफ़ करें, और wait handles pool के इस्तेमाल खत्म होने तक जीवित रखें। ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices. इस पर कि DllMain loader lock पकड़े बुलाई जाती है, जो सुरक्षित बुलाने योग्य API पर गंभीर सीमाएँ डालती है; कि DllMain में अन्य threads से synchronize करना deadlock लाता है; कि LoadLibrary बुलाना निषिद्ध कार्यों की सूची में है; कि DLL unload पर DllMain में thread खत्म होने की wait उस thread के DLL_THREAD_DETACH पहुँचाने loader lock लेने के प्रयास से deadlock होती है; कि आदर्श DllMain खाली stub के निकट है, initialization जितना हो टालें; और loader lock शीर्ष पर lock hierarchy परिभाषित करने पर। ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. C standard library conformance तालिका पर, दिखाते हुए C11 threads (threads.h) Visual Studio 2022 17.8 से supported; stdatomic.h experimental ( /experimental:c11atomics option के पीछे); और C11 / C17 compiler support को Visual Studio 2019 16.8 या बाद के साथ मिलते Windows SDK की आवश्यकता। ↩ ↩2
-
Microsoft Learn, Interlocked Variable Access. इस पर कि सही aligned 32-bit variable का सादा पढ़ना या लिखना atomic है, पर पहुँच का synchronization (ordering) guarantee नहीं; कि 64-bit variable का सादा पढ़ना-लिखना 64-bit Windows पर atomic है पर 32-bit Windows पर guarantee नहीं; और कि अन्य आकार के variables किसी platform पर atomic guarantee नहीं। ↩ ↩2
-
Microsoft Learn, _beginthread, _beginthreadex. इस पर कि _beginthreadex _beginthread से सुरक्षित क्यों है: _beginthread से बना thread जल्दी खत्म हो तो लौटा handle invalid (या दूसरे thread की ओर) रह सकता है; _beginthreadex का handle caller CloseHandle से बंद करे और उसकी validity guarantee है; _beginthreadex handle synchronization API को देने देता है; thread function __stdcall calling convention से thread exit code लौटाता है; और multithreaded CRT से link आवश्यक है। ↩
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents मिटाना
C++ में multithreading वह संसार है जहाँ data race undefined behavior बन जाता है। यह लेख std::thread के destructor का pitfall, jthread और ...
Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
Multithreaded .NET/C# code को कभी-कभी crash या hang होने से बचाने वाले design नियमों का practical सार: thread स्वयं न बनाकर Task पर चलें,...
Practical multithreading best practices: Java संस्करण — virtual thread युग की practices
Java में multithreading की स्थापित practice यह है कि thread सीधे न बनाएँ, बल्कि ExecutorService और virtual threads पर चलें। यह लेख practi...
Win32 Thread Pool API — CreateThreadpoolWork से thread बनाए बिना concurrency
क्या आपके native code में CreateThread calls बिखरी पड़ी हैं? यह लेख Vista में redesign की गई Win32 thread pool API — work, timer, wait और...
DllMain और Loader Lock — DLL initialization में "कुछ मत करो" कहा जाने का असली कारण
DllMain से LoadLibrary क्यों न call करें और दूसरे thread से synchronize न करें। Primary sources से यह लेख बताता है कि loader lock हर DLL ...
संबंधित विषय
ये पृष्ठ विषय को सेवाओं और निर्णयों के व्यापक संदर्भ में रखते हैं।
Windows के तकनीकी विषय
Windows विकास, बग जाँच और मौजूदा संपत्तियों के उपयोग का प्रवेश-द्वार।
इस विषय से जुड़ी सेवाएँ
यह लेख निम्नलिखित सेवाओं से सीधे जुड़ा है।
Windows ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- CreateThread इस्तेमाल करूँ या _beginthreadex?
- हर उस thread के लिए _beginthreadex इस्तेमाल करें जो C runtime library (CRT) के functions बुलाता है। _beginthreadex thread शुरू करने से पहले CRT को per-thread चाहिए internal data initialize करता है। Official documents साफ़ कहते हैं कि यदि CreateThread से बना thread CRT functions बुलाए, तो कम memory पर CRT process terminate कर सकता है। व्यवहार में C app के threads लगभग हमेशा कहीं CRT functions (printf, malloc, strtok आदि) बुलाते हैं, इसलिए नियम «हमेशा _beginthreadex» याद रखना हानिकारक नहीं। _beginthread (बिना ex) से भी बचें — उसमें pitfall है कि बना thread जल्दी खत्म हो तो लौटा handle invalid हो सकता है, इसलिए _beginthreadex चुनें, जिसका handle synchronization API को दिया जा सकता है।
- क्या मैं TerminateThread से thread नहीं रोक सकता?
- नहीं, नहीं करना चाहिए। TerminateThread target thread को कोई user-mode code चलाने दिए बिना मिटा देता है, इसलिए यदि वह thread critical section पकड़े था तो वह कभी नहीं छूटता, यदि heap से memory allocate कर रहा था तो heap lock पकड़ा रहता है, और यदि किसी DLL की global state बदल रहा था तो वह state corrupt हो जाती है। Official documents स्पष्ट कहते हैं कि यह «एक खतरनाक function है जिसका इस्तेमाल केवल सबसे चरम मामलों में होना चाहिए», और code analysis इसे warning C6258 भी लगाता है। सही stop cooperative shutdown है: stop event बनाएँ, हर thread WaitForSingleObject / WaitForMultipleObjects से उस event पर नज़र रखे, और हर thread स्वयं साफ़ करके स्वयं खत्म हो।
- मैं process के भीतर mutual exclusion के लिए Mutex इस्तेमाल कर रहा था। क्या गलत है?
- चलता है, पर performance का बड़ा दाम पड़ता है। Win32 Mutex हमेशा kernel object है, इसलिए हर acquire और release kernel mode में transition करती है। एक process के भीतर mutual exclusion के लिए SRW lock या CRITICAL_SECTION — जो user mode में रहते हैं और केवल contention पर kernel wait में गिरते हैं — कहीं तेज़ हैं, और official documents process-internal synchronization के लिए Mutex को स्पष्ट रूप से «सामान्य गलती» कहते हैं। Mutex तब काम आता है जब named object के रूप में processes के आर-पार mutual exclusion चाहिए, या जब WaitForMultipleObjects से अन्य kernel objects के साथ उसकी wait करनी हो।
- क्या Windows पर C11 के threads.h और stdatomic.h इस्तेमाल हो सकते हैं?
- MSVC में C11 threads (threads.h) Visual Studio 2022 17.8 से supported हैं ( /std:c11 और मिलता Windows SDK चाहिए)। stdatomic.h अभी भी experimental माना जाता है और /experimental:c11atomics option माँगता है (अगस्त 2026 की official conformance तालिका के अनुसार)। यदि portability सर्वोच्च प्राथमिकता है तो यह व्यवहार्य विकल्प है, पर केवल-Windows codebase के लिए Win32 API (_beginthreadex, SRW lock, condition variable, Interlocked functions) लिखना उपलब्ध जानकारी और track record के हिसाब से यथार्थवादी चुनाव है।
- क्या volatile जोड़ने से shared flag सुरक्षित हो जाता है?
- नहीं। C का volatile केवल compiler optimization जैसे register में value cache करना दबाता है — यह न तो operation की atomicity guarantee देता है न processors के आर-पार memory ordering। सही aligned 32-bit variable का सादा पढ़ना या लिखना Windows पर स्वयं atomic है, पर «पढ़ो, जोड़ो और वापस लिखो» अलग चरणों में टूटता है, और आसपास की memory operations के सापेक्ष क्रम की भी guarantee नहीं। Shared counter या flag update करने के लिए Interlocked परिवार इस्तेमाल करें। अधिकतर Interlocked functions full memory barrier लाते हैं, इसलिए ordering की guarantee भी साथ मिलती है। जब कई variables एक साथ बचाने हों, SRW lock या CRITICAL_SECTION लें।