Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents मिटाना
· अद्यतन तिथि: · Go Komura · Windows, multithreading, C++, Visual Studio, business app, bug investigation, design
संशोधन इतिहास (पहला संस्करण, 2 Aug 2026 को प्रकाशित)
- पहला प्रकाशन
इस लेख का हवाला दें(DOI: 10.5281/zenodo.22175860)
यह लेख Zenodo पर संग्रहीत है। नीचे वह DOI है जो हमेशा नवीनतम संस्करण की ओर ले जाता है, और वह DOI भी जो आपके पढ़े जा रहे संस्करण से बँधा है।
Go Komura (2026). Practical multithreading best practices: C++ संस्करण — RAII और jthread से structure द्वारा accidents मिटाना. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22175860 https://comcomponent.com/hi/blog/multithreading-best-practices-cpp/
- DOI (नवीनतम संस्करण)
- 10.5281/zenodo.22175860
- DOI (यह संस्करण)
- 10.5281/zenodo.22175861
«C# में ठीक चलने वाला design C++ में port करते ही कभी-कभी crash करने लगा।» «हमने std::thread इस्तेमाल किया, और exception फेंके जाने पर पूरा app terminate से तुरंत मर गया।» «हम volatile bool flag से रोक रहे थे, पर केवल release build में वह रुकता नहीं था।» — C++ multithreading वह ख़तरा लाती है जो managed भाषाओं में बस नहीं है: data race, जैसी है, undefined behavior (UB) है। बात केवल corrupt value पढ़ने की नहीं; compiler की optimization मान्यताएँ ढह जाती हैं, और आप ऐसी स्थिति में पहुँचते हैं जहाँ शाब्दिक रूप से कुछ भी हो सकता है।
यह लेख हमारी practical multithreading श्रृंखला का C++ संस्करण है। आधुनिक C++ (C++17/20) में business app, device-control software और DLL लिखने वाले developers के लिए, यह multithread design के सामान्य सिद्धांतों — thread सीधे न जोड़ें, shared mutable state घटाएँ, lock discipline लागू करें, रुकने का तरीका सबसे पहले design करें — को C++ और Windows के tools में उतारता है, C++-विशिष्ट pitfalls के साथ, अगस्त 2026 तक के primary sources पर आधारित। इसे अकेले खड़ा रहने के लिए लिखा गया है। अन्य भाषाओं के लिए निकाले गए वही सिद्धांत «.NET संस्करण», «C संस्करण» और «Java संस्करण» में भी हैं।
1. पहले निष्कर्ष
- C++ में data race «corrupt value पढ़ सकते हैं» नहीं — वह undefined behavior है। Code में एक भी unsynchronized shared mutable access न छोड़ना निरपेक्ष requirement है, अन्य भाषाओं से अधिक।1
std::threadनंगा इस्तेमाल न करें। Thread अभी joinable रहतेstd::threadका destructor चले तोstd::terminateprocess को तुरंत मार देता है। C++20 काstd::jthreaddestructor में स्वतः join करता है और उसमें stop request तंत्र (stop_token) बना होता है।23- Lock हमेशा RAII से पकड़ें। हाथ से
mtx.lock()लिखना बंद करें;lock_guard/scoped_lockइस्तेमाल करें। Exception फेंके जाने पर भी destructor lock विश्वसनीय रूप से छोड़ता है। कई locks एक साथ लेने परscoped_lockdeadlock-avoidance algorithm से सँभालता है।4 volatilesynchronization का औज़ार नहीं। Shared flags और counters के लिएstd::atomic, कई variables एक साथ सुरक्षित करने के लिएstd::mutex।std::atomicindivisibility औरmemory_orderपर आधारित ordering दोनों देता है।5- Rendezvous wait
condition_variableके predicate-रूपwaitसे करें। Condition variables spurious wakeup (सूचना बिना जागना) के अधीन हैं, इसलिए predicate के बिनाwaitbugs का अड्डा है।6 - Thread कैसे रोके, उसका मूल रूप
jthread+stop_token(C++20) है। उससे पहले के परिवेश मेंstd::atomic<bool>प्लस condition variable से cooperative stop हाथ से बनाएँ। Thread का forced termination C++ की दुनिया में बस अस्तित्वहीन मानें।3 std::asyncइस्तेमाल से पहले जानें किfutureका destructor block कर सकता है। लौटा value फेंक दें तो sequential execution जैसा ही प्रभाव मिलता है।7- Win32 synchronization objects केवल «Win32 wait API के साथ काम» और «cross-process» परिदृश्यों के लिए जगह पाते हैं। बाकी जगह standard library के विरुद्ध लिखना portability और maintainability के लिए बेहतर है।8
2. Multithreading कठिन क्यों है — race condition, deadlock और undefined behavior
संक्षेप में, multithreading जो समस्याएँ लाती है वे भाषा से निरपेक्ष दो प्रकार की हैं।
Race condition वह bug है जिसमें परिणाम इस पर depend करता है कि कई threads किसी code खंड तक किस क्रम में पहुँचते हैं। Classic उदाहरण shared counter है: एक अभिव्यक्ति ++count machine-code स्तर पर तीन चरणों में टूटती है — पढ़ना, जोड़ना, वापस लिखना। दो threads उन तीन चरणों में एक साथ घुसें तो एक thread का जोड़ दूसरे की write-back से overwrite होकर खो जाता है। परिणाम हर 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 हुईं,<br/>फिर भी count = 11 — Thread A का जोड़ खो गया
चित्र 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 लेने की wait"| B["Thread B<br/>Lock 2 पकड़े"]
B -->|"Lock 1 लेने की wait"| A
चित्र 2: Deadlock की circular wait। Wait के तीर जैसे ही loop बनाते हैं, उस loop का हर thread सदा रुक जाता है
दोनों को असुविधाजनक बनाने वाली बात time-dependence है। Development मशीन पर दसियों हज़ार runs में एक बार लगने वाला interleaving ग्राहक की मशीन पर, अलग core count और अलग timing के साथ, हर दिन हो सकता है। «Debugger लगे होने पर reproduce नहीं होता» और «log जोड़ते ही गायब हो गया» दोनों इसलिए होते हैं क्योंकि स्वयं observation timing बदल देता है — यही race-bug का classic व्यवहार है। इसीलिए इस लेख का हर सिद्धांत एक दिशा की ओर है: सही synchronize करने की चिंता से पहले, synchronization चाहिए वाले स्थान घटाएँ।
2.1. C++ में data race सीधे undefined behavior है
उस पर C++ में एक और परत है जो अन्य भाषाओं में नहीं। C++ standard के अनुसार, कई threads बिना synchronization के एक ही memory location पर पहुँचें और कम से कम एक लिखे, तो वह data race है, और वह undefined behavior है। C++ Core Guidelines का concurrency अध्याय (CP.2, «Avoid data races») इसे सबसे पहली निरपेक्ष नियम के रूप में रखता है।1 Undefined behavior «पुराना value या नया पढ़ सकते हैं» जैसी नरम कहानी नहीं। Compiler इस आधार पर optimize करता है कि data race नहीं है, इसलिए source से कभी न अनुमानित व्यवहार — loop से शर्त जाँच गायब होना, writes का reorder या merge — वैध रूप से होता है। Classic accident जिसमें «volatile bool stop flag केवल release build में fail होता है» ठीक इसी का textbook मामला है।
2.2. RAII नींव है
C++-विशिष्ट एक और आधार exceptions और resource management है। C++ में finally नहीं; उसके स्थान पर RAII (destructor से automatic release) है, और multithreading के tools इसी मान्यता पर design हैं कि आप उसे इस्तेमाल करेंगे। «Lock object के जीवन से सँभालें»; «thread का join भी object के जीवन से guarantee दें» — उस परंपरा पर चलना सुरक्षित multithreaded C++ लिखने की नींव है।
3. Thread कैसे शुरू करें — thread का pitfall, और jthread
3.1. std::thread का destructor «accident पैदा करने के लिए design है»
std::thread का जाना-माना pitfall है। Thread अभी joinable रहते (न join न detach) destructor चले तो std::terminate बुलाया जाता है और process तुरंत मर जाता है।9
void process()
{
std::thread worker([]{ HeavyWork(); });
DoSomething(); // ← यदि यहाँ exception फेंका जाए...
worker.join(); // ← join कभी नहीं पहुँचता; worker का destructor terminate बुलाता है
}
इसे exception-safe बनाने के लिए try/catch से join guarantee करनी पड़ती थी — RAII भाषा में विकृत स्थिति जहाँ केवल thread हाथ से सँभालने पड़ते थे। C++20 का std::jthread इसे सुलझाता है। क्योंकि उसका destructor स्वतः stop request जारी करता है फिर join करता है, ऊपर का code std::jthread पर switch करते ही exception-safe हो जाता है।2 MSVC पर <stop_token> और jthread Visual Studio 2019 16.9 से उपलब्ध हैं।3
flowchart TB
T["Thread शुरू हो चुका है"] --> Q{"Scope निकलने पर<br/>क्या होता है?"}
Q -->|"std::thread<br/>न join न detach"| X["std::terminate<br/>process तुरंत मरता है"]
Q -->|"std::thread<br/>पहले ही join"| OK1["सुरक्षित join"]
Q -->|"std::jthread - C++20"| OK2["Automatic request_stop + join<br/>exception फेंके जाने पर भी सुरक्षित"]
चित्र 3: Thread object का जीवन और वह कैसे समाप्त होता है। std::thread specified है कि join भूलें तो तुरंत मरे, इसलिए C++20 से jthread को default बनाएँ
नियम के रूप में detach() इस्तेमाल न करें। Join का हर साधन खो चुका thread shutdown crash का classic कारण बनता है, process निकलते समय static variables और heap के destruction से दौड़ता हुआ।
3.2. «Thread स्तर से ऊपर» के tools — async, future और parallel algorithms
.NET संस्करण का सिद्धांत «thread स्वयं न बनाएँ» C++ में इन tools पर पड़ता है।
std::async+std::future: एकबारगी async work और उसका परिणाम पाने के लिए। पर महत्वपूर्ण विशेषता:std::asyncसे शुरू work से बँधाfuture(या अंतिमshared_future) work अधूरा रहते destructor चलने पर पूरा होने तक block करता है।7 वास्तव मेंstd::launch::asyncसे शुरू काम के लिए लौटा future फेंकना उस बिंदु पर synchronous execution के बराबर है। बदतर, launch policy न बताएँ तो implementation default रूप सेdeferred(lazy execution) चुन सकता है, जिसमेंget()/wait()कोई न बुलाए तो काम चलता ही नहीं और चुपचाप गायब हो जाता है। Concurrent execution guarantee चाहिए तोstd::launch::asyncस्पष्ट करें, और मालिक future का जीवन सँभाले।- PPL - Parallel Patterns Library -
concurrency::parallel_for/parallel_for_each: Collection के हर element पर काम parallel लागू करना। पर एक iteration का काम बहुत छोटा हो तो fork/join overhead लाभ खा जाता है, इसलिए नियमतः बाहरी loop पर parallel करें।10 - C++17 के parallel algorithms -
std::execution::par: MSVC पर प्रमुख algorithms parallel हैं (सभी नहीं)।11 ध्यान दें कि execution policy के तहत element processing से exception निकले तोstd::terminateबुलाया जाता है। Callback के भीतर अपना exception boundary (try/catch) रखना खंड 6 की thread boundary जैसी ही सोच है।
अन्यत्र खींची रेखा — «I/O की wait thread जोड़कर हल नहीं होती» — बिना परिवर्तन लागू रहती है। Native Windows code के लिए OVERLAPPED I/O और IOCP वे tools हैं जो वह काम पकड़ते हैं (mechanics के लिए देखें «Windows I/O की गहराई, भाग 2»)।
4. Shared mutable state घटाना — partitioning, pass-by-value, const और queues
Contention तभी उठती है जब «कई threads» और «shared mutable data» दोनों हों। Threads की संख्या requirements तय करती हैं, इसलिए design काट सकता है sharing। साधन तीन परिवारों में पड़ते हैं — partitioning, immutable बनाना, और data hand-off — और C++ में प्रत्येक ऐसे लिखते हैं।
Partition करें। Parallel aggregation जैसे काम में हर thread shared sum में लिखने के बजाय हर thread को अपना local accumulator दें और अंत में एक बार मिलाएँ। Shared value पर write «हर iteration» से «प्रति thread एक बार» तक गिरता है, synchronization लागत और contention window दोनों को परिमाणों में काटता है। वह एक merge चरण std::mutex से या std::atomic पर fetch_add से — दोनों ठीक।
Pass by value करें। Thread को चाहिए data आरंभ पर copy (या move) से दें तो वह data thread का exclusive हो जाता है, और synchronization नहीं चाहिए। Lambda को reference से पकड़ना ([&]) फिर समाप्त जीवन वाले variable को छूना आम accident है, इसलिए threads को दी lambda स्पष्ट capture, नियमतः copy या move इस्तेमाल करें। हाँ, «copy हुआ, इसलिए exclusive» तभी जब value pointer या shared_ptr जैसे alias रहित गहरा value-graph हो। Raw pointer वाली संरचना copy करने पर भी वह जिस ओर इशारा करता है shared रहता है।
const के रूप में share करें। केवल पढ़े जाने वाले data किसी भी संख्या के threads से एक साथ पढ़ना सुरक्षित है। Config values, master data, computation input आदि निर्माण के बाद न लिखे जाने वाले const shared (std::shared_ptr<const Config> जैसे) बनाएँ तो बिना synchronization share हो सकते हैं। एक चेतावनी: shared_ptr<const T> जो मना करता है वह केवल उस handle से mutation है। कहीं और non-const alias बचा हो, या mutable member फिर लिखा जाए, contention रहती है — इसलिए वह भी design करें, «निर्माण खत्म होते ही non-const references छोड़ें और बाद में कोई न लिखे» तक। केवल यह तय करना कि «बदलाव चाहिए तो जगह पर बदलने के बजाय नया object बनाकर बदलें» एक mutable state हटाता है जिसे अन्यथा सुरक्षित रखना पड़ता (स्वयं swap के lifetime management के लिए खंड 5.2 की चेतावनी देखें)।
Queue से hand-off करें। Threads के बीच data flow shared variable के बजाय producer/consumer queue से चलाएँ। C++ standard में channel type नहीं, इसलिए std::mutex + std::condition_variable से छोटी queue लिखना स्थापित pattern है।
template <typename T>
class BlockingQueue {
public:
explicit BlockingQueue(std::size_t capacity) : capacity_(capacity)
{
if (capacity == 0) // क्षमता 0 वह pitfall है जहाँ हर Push सदा wait करता है
throw std::invalid_argument("capacity must be positive");
}
// भरा हो तो जगह (या stop request) तक wait। false मतलब stop request।
bool Push(T item, std::stop_token st)
{
{
std::unique_lock lock(mtx_);
if (!not_full_.wait(lock, st, [this]{ return queue_.size() < capacity_; }))
return false; // Stop request से जगाया गया
if (st.stop_requested()) // जगह और stop साथ हों तो stop प्राथमिक,
return false; // और रुकना शुरू होने के बाद push अस्वीकार
queue_.push(std::move(item));
}
not_empty_.notify_one(); // Lock के बाहर notify करें
return true;
}
// Stop request (stop_token) या item आने तक wait। रुके पर nullopt।
std::optional<T> Pop(std::stop_token st)
{
std::optional<T> item;
{
std::unique_lock lock(mtx_);
if (!not_empty_.wait(lock, st, [this]{ return !queue_.empty(); }))
return std::nullopt; // Stop request से जगाया गया
if (st.stop_requested()) // Item और stop साथ हों तो stop प्राथमिक,
return std::nullopt; // और रुकना शुरू होने के बाद नया काम न शुरू करें
item = std::move(queue_.front());
queue_.pop();
}
not_full_.notify_one();
return item;
}
private:
const std::size_t capacity_;
std::mutex mtx_;
std::condition_variable_any not_empty_; // condition_variable_any, stop_token-aware wait के लिए
std::condition_variable_any not_full_;
std::queue<T> queue_;
};
यहाँ दो design बिंदु हैं। पहला, क्षमता सीमित करें और भरा होने पर producer पक्ष को wait कराएँ। ऊपरी सीमा रहित queue उन व्यवस्थाओं में time bomb बनती है जहाँ production consumption से आगे निकलता है: वह «चलता रहता है», पर memory बढ़ती जाती है। भरा होने पर Push का block प्राकृतिक backpressure बनता है, overload को यंत्रवत ऊपर भेजता है। दूसरा, condition variables spurious wakeup (सूचना बिना जागना) के अधीन हैं, इसलिए wait हमेशा predicate से बुलाएँ। wait का predicate रूप भीतर «शर्त सत्य होने तक loop» तर्क आपके लिए चलाता है।6
5. Lock discipline — RAII और scoped_lock
Shared mutable state घटाने के बाद भी अक्सर शून्य तक नहीं पहुँचते। जो shared बचा उसके लिए mutual exclusion इस्तेमाल करें, पर बिना discipline lock केवल contention छिपाता है।
पहले, lock की इकाई «code का टुकड़ा» नहीं «data» सोचें। सुरक्षित करने वाले हर mutable data समूह को एक mutex दें (उसे private member बनाएँ, बाहर न खोलें), और उस data को छूने वाले हर स्थान पर वही mutex लें — इस तालिका का टूटा रूप ही अधिकांश race bugs वास्तव में हैं। और lock पकड़े केवल वही कर सकते हैं जो वह data पढ़ना-लिखना है। Lock पकड़े file I/O, network call और callback (बाहरी code में call) न केवल पकड़ बढ़ाते हैं — वे रास्ता खोलते हैं जहाँ call किया गया दूसरा lock लेने की कोशिश कर deadlock करता है। Lock के बाहर तैयार करें, और lock के भीतर केवल बदलें मूल रूप है।
5.1. हाथ से lock()/unlock() वर्जित
std::mutex के lock() / unlock() सीधे बुलाने वाला code exception या जल्दी लौटने पर lock नहीं छोड़ पाता। Lock लेना और छोड़ना हमेशा RAII wrapper पर छोड़ें।
| Wrapper | इस्तेमाल |
|---|---|
std::lock_guard |
एक mutex ठीक एक scope भर पकड़े — सबसे मूल रूप |
std::scoped_lock (C++17) |
कई mutexes एक साथ लेता है। क्रम समस्या deadlock-avoidance algorithm से सुलझाता है4 |
std::unique_lock |
बीच में खोलकर फिर बंद करना हो, या condition_variable::wait को देना हो |
दो या अधिक locks हों तो thread के अनुसार लेने का क्रम पलटना classic deadlock pattern है (चित्र 2 की circular wait ठीक ऐसे जन्म लेती है)। सुधार नियम बनाना कि «हर thread lock एक ही क्रम में ले», पर जब उन्हें एक ही समय ले रहे हों, C++ का बेहतर उत्तर है: कई mutexes std::scoped_lock को साथ दें और library deadlock-free लेने का क्रम guarantee करती है।4 दो objects के बीच transfer जैसे «दोनों lock» चाहिए स्थितियों में उन्हें अलग-अलग कभी न लें — हमेशा साथ लें।
void Transfer(Account& from, Account& to, int amount)
{
if (&from == &to) return; // उसी खाते के लिए कुछ न करें (नीचे टिप्पणी देखें)
std::scoped_lock lock(from.mtx, to.mtx); // दोनों साथ; क्रम library सुलझाती है
from.balance -= amount;
to.balance += amount;
}
ऊपर पहचान जाँच सजावट नहीं। वही Account from और to दोनों बने तो आप एक ही non-recursive mutex scoped_lock को दो बार देते हैं, जिससे hang या undefined behavior होता है। «दोनों lock» करने वाले हर function पर वही-object exclusion हमेशा लगाएँ।
«अक्सर पढ़े, कम लिखे» data के लिए std::shared_mutex (C++17) reader/writer lock के रूप में इस्तेमाल हो सकता है।12 recursive_mutex वह type है जो «उसी thread का फिर लेना न टूटे» के लिए design है, पर recursive लेने वाला design अक्सर संकेत है कि lock की ज़िम्मेदारी सीमा धुँधली हो गई — पहले structure फिर देखें।
5.2. atomic की सही भूमिका
std::atomic एक variable पर atomic operation देता है, प्लस memory_order पर आधारित ordering।5 उसकी जगह .NET संस्करण के Interlocked जैसी स्थितियों में है: एक variable update करना, जैसे counter या flag। वह कई variables एक साथ consistent नहीं रख सकता, इसलिए उसके लिए std::mutex पर लौटें।
Raw pointer की swap (std::atomic<T*>) का अपना pitfall है। Swap स्वयं atomic होने पर भी, पुराने object का जीवन बदले जाने के बाद कोई सुरक्षित नहीं करता। Writer बदलकर delete करे ठीक पहले reader पुराना pointer load करे तो मुक्त memory पर पहुँच मिलती है। C++ में «immutable object बदलकर share करें» design करना हो तो वह साधन चुनें जो lifetime management के साथ आता है — lock-safe std::shared_ptr<const T> बदलना, या C++20 का std::atomic<std::shared_ptr<T>>।
और दोहराना: volatile thread-synchronization का औज़ार नहीं। स्वयं memory_order बताने वाला lock-free programming specialist क्षेत्र है, जिसमें default (seq_cst) से ढीला करने का वैध कारण और सही करने की जाँच दोनों चाहिए। Business app में या तो default रखें या शुरू से mutex से लिखें।
6. रुकने का design — stop_token और cooperative stop
Multithread design की review में पहला प्रश्न «यह कैसे रुकता है?» है। और C++ में thread को बाहर से सुरक्षित रोकने का साधन नहीं (Win32 का TerminateThread कितना ख़तरनाक है C संस्करण में विस्तार से है)। इसलिए thread कैसे रुकता है C++ के tools से cooperative stop के इर्द-गिर्द बनाना पड़ता है — रोकने वाला पक्ष केवल request जारी करता है; thread स्वयं तय करता है कब और कैसे खत्म हो, उस बिंदु पर जो चीज़ें साफ़ छोड़ता है; और join पूरा होना ही «रुक गया» गिना जाता है।
C++20 में std::jthread में stop तंत्र बना है। request_stop() बुलाने से thread function को मिला std::stop_token पर stop request उठता है, और loop उसे जाँचता है। condition_variable_any का wait stop_token सीधे ले सकता है, इसलिए «काम आने की wait करता thread» भी stop request से तुरंत जगाया जा सकता है (खंड 4 का BlockingQueue::Pop ठीक यही रूप है)।
class Worker {
public:
void Start()
{
if (thread_.joinable()) // पहले से चलते दोहरा Start अस्वीकार करें।
throw std::logic_error("already running"); // अस्वीकार के बजाय assign करें तो नया
// thread चलने लगेगा, और पुराने के रुकने
// की wait करते दो workers
// साथ-साथ चलेंगे
thread_ = std::jthread([this](std::stop_token st) {
try {
while (!st.stop_requested()) {
if (auto item = queue_.Pop(st)) { // Stop request पर भी जागता है
try {
Process(*item, st); // भीतर block कर सकने वाले काम में भी st दें
} catch (...) {
ReportError(std::current_exception()); // एक failure दर्ज कर जारी रखें
}
}
}
} catch (...) {
// Thread boundary की अंतिम रक्षा रेखा (Pop या move की failure भी पकड़ती है)।
// यहाँ से exception निकले तो std::terminate पूरी process गिराता है,
// इसलिए ReportError स्वयं कभी न फेंके
ReportError(std::current_exception());
}
});
}
// स्पष्ट Stop नहीं चाहिए:
// Worker का destructor -> jthread का destructor -> request_stop() + join()
private:
BlockingQueue<WorkItem> queue_{100}; // Capacity-limited (खंड 4)
std::jthread thread_;
};
flowchart TB
OWNER["रोकने वाला पक्ष<br/>- jthread का destructor, या request_stop"] -->|"Stop request"| ST["stop_token"]
ST --> P["Computation loop:<br/>stop_requested() जाँचता है"]
ST --> W["Wait करता thread:<br/>condition_variable_any::wait(lock, st, pred)<br/>तुरंत जागता है"]
P --> E["साफ़ कर स्वयं लौटता है"]
W --> E
E --> J["join rendezvous पूरा करता है<br/>अब ही रुक गया कह सकते हैं"]
चित्र 4: C++20 cooperative stop। रोकने वाला पक्ष केवल request जारी करता है; thread स्वयं तय करता है कैसे खत्म हो; join पूरा होना ही रुका माना जाता है
एक और बात: worker के भीतर try/catch छोड़ा नहीं जा सकता। jthread जो exception-safe बनाता है वह join, और केवल join है — thread function से exception निकले तो std::terminate process गिराता है, ठीक std::thread की तरह। Thread boundary पर स्पष्ट तय करें कि एक काम की failure कैसे सँभालें (दर्ज कर जारी रखें, या error channel से मालिक को बताएँ)।
उसी कारण ध्यान दें कि Process को भी stop_token दिया जाता है। एक काम का processing भीतर block करे (network wait, लंबी computation आदि) और वह बिंदु stop request न देख सके, तो destructor का निहित join उस एक item के खत्म होने की सदा wait करेगा। Cooperative stop तभी टिकती है जब token हर wait स्थान तक पहुँच चुका हो। काम में बाहरी call हो जो interrupt न हो सके, तो timeout लगाएँ और एक item कितनी देर चल सकता है उसकी ऊपरी सीमा रखें।
C++17 से पहले के परिवेश में वही रूप std::atomic<bool> stop flag प्लस condition_variable के notify_all से हाथ से बनाते हैं। यहाँ मुख्य बात stop-flag जाँच को condition variable के predicate में मोड़ना है — केवल flag उठाएँ और notify करना भूलें तो wait करता thread कभी नहीं जागेगा।
7. Windows-विशिष्ट चिंताएँ — Win32 API की सीमा
7.1. Standard library और Win32 synchronization objects के बीच चुनाव
Microsoft documents portability-first C++ code के लिए std::mutex / std::shared_mutex सुझाता है, और Win32 synchronization objects की जगह «जब Win32 wait API चाहिए» और «cross-process synchronization» बताता है।8
| स्थिति | चुनाव |
|---|---|
| सामान्य intra-process mutual exclusion | std::mutex + RAII (default) |
| बहुत पढ़ना, कम लिखना | std::shared_mutex |
WaitForMultipleObjects से कई objects एक साथ wait |
Event और mutex जैसे Win32 kernel objects |
| Cross-process mutual exclusion / notification | Named mutex, event, semaphore |
| Win32 API सीधे इस्तेमाल कर intra-process lock | SRW lock (recursion चाहिए तभी CRITICAL_SECTION)8 |
Processes के आर-पार shared memory access exclude करने के ठोस design के लिए देखें «Shared memory के pitfalls और practical best practices»।
7.2. DllMain के भीतर thread न छुएँ
DLL लिखते समय गंभीर बाधा loader lock है। DllMain loader lock पकड़े बुलाया जाता है, इसलिए उसके भीतर दूसरे thread से synchronize करना, thread खत्म होने की wait, या LoadLibrary बुलाना deadlock या unexpected व्यवहार पैदा करता है। Thread शुरू या join करने वाला कोई initialization DllMain से बाहर, स्पष्ट initialization function में ले जाएँ।13
7.3. UI thread और COM apartment
Windows desktop app की एक मज़बूत बाधा भाषा से निरपेक्ष लागू होती है: केवल वह thread जिसने window या control बनाया — UI thread — उसे छू सकता है। Windows window messages उस thread की message queue में पहुँचाता है जिसने वह window बनाई, इसलिए UI का निर्माण और संचालन उसी thread पर केंद्रित होना चाहिए। Worker thread से screen update करना हो तो सीधे न छुएँ — PostMessage (async) से UI thread से कहें, और UI thread की window procedure में सँभालें। Synchronous रूप SendMessage बुलाना, जबकि UI thread उस worker के खत्म होने की wait कर रहा हो, deadlock पैदा करता है जिसमें प्रत्येक दूसरे की wait करता है, इसलिए worker से सूचनाओं के लिए async रूप default बनाएँ। STA/MTA, जहाँ COM शामिल है, «COM STA/MTA मूल बातें — threading model और hang से कैसे बचें» में है। यह भी ध्यान दें कि /clr से compile C++/CLI code में <thread> और <mutex> जैसे standard thread headers blocked हैं।14
8. Verification और debug — इस मान्यता पर तैयारी कि reproduce नहीं होगा
Race bugs खोजने के लिए testing पर भरोसा नहीं कर सकते, क्योंकि सामान्य test «संयोग से race न हुआ» run को pass गिनता है। तैयारी तीन परतों में सोचें।
पहली रक्षा रेखा अब तक के design सिद्धांत हैं, ठीक जैसे हैं। Review में तालिका से पुष्टि करें: कौन सा mutable data shared है, कौन सा mutex प्रत्येक टुकड़ा सुरक्षित करता है, कई locks लेने का क्रम unique है या नहीं (या scoped_lock से साथ लिए गए), और stop पथ कहाँ है। वह design जिसके लिए यह तालिका न लिख सकें अधूरा है, चाहे अभी कितना अच्छा चले।
दूसरा, abnormal states छिपाने के बजाय observable बनाएँ। जो lock कभी fail न होना चाहिए उस पर timed_mutex का try_lock_for या condition_variable का wait_for से timeout लगाएँ, और timeout को anomaly के रूप में log करें — वह शाश्वत hang को पता लगाने योग्य failure बनाता है। Thread boundary के try/catch (खंड 6) में पकड़े exceptions हमेशा log करें। मैदान में hang या crash हो तो dump लें, हर thread का stack जाँचें, और देखें कि उनकी lock wait cycle तो नहीं बनाती। Dump और logging की व्यवस्था «Windows app crash के लिए logging और dump capture design» में है।
तीसरा, load के नीचे हिलाएँ। Cores से अधिक parallelism से लंबे समय चलाना, processing क्रम random करना, और कृत्रिम delay डालना practical stress-test तकनीकें हैं जो development मशीन पर race «jackpot» लगाना आसान बनाती हैं। Debug build में गायब bugs अक्सर optimized release build में भारी load के नीचे आसानी से reproduce होते हैं।
9. सार — C++ checklist
हर भाषा के साझा सिद्धांतों — thread सीधे न बनाएँ, shared mutable state न्यूनतम करें, lock और data का one-to-one मेल, cooperative stop — पर C++-विशिष्ट जाँचें चढ़ाएँ।
- क्या
std::threadनंगा इस्तेमाल हो रहा है (क्याjthreadहो सकता है? Exception पथ पर भी join guarantee है?) - क्या
detach()इस्तेमाल नहीं हो रहा? - क्या lambda captures स्पष्ट हैं, और reference-captured variable thread से अधिक जीता है?
- क्या विश्वास से कह सकते हैं कि कहीं एक भी unsynchronized shared mutable access (= undefined behavior) नहीं?
- क्या हाथ से
lock()/unlock()नहीं, और कई locksscoped_lockसे साथ लिए जाते हैं? - क्या हर
condition_variable::waitpredicate के साथ है? - क्या shared flags के लिए
volatileनहीं (उसके स्थान परstd::atomicहै)? - क्या stop पथ
stop_token(या atomic flag प्लस notification) के इर्द-गिर्द design है, join पूरा होना rendezvous की पुष्टि करता है? - क्या
std::asyncकाfutureनहीं फेंका जा रहा? - क्या
DllMainthread शुरू, synchronize या join से मुक्त है?
Multithreaded C++ undefined behavior के किनारे के ठीक साथ चलने वाला काम है, पर उसे पलटें तो मतलब है कि RAII और standard library की परंपराओं के साथ ईमानदारी से चलना उस किनारे से वास्तविक दूरी रखता है। jthread, scoped_lock, predicate-रूप wait, atomic — इन tools में सही default चुनना, C++ में, design सिद्धांतों का अभ्यास स्वयं है।
संबंधित लेख
- Practical multithreading best practices: .NET संस्करण — और thread जोड़ने से पहले क्या तय करें
- Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
- Practical multithreading best practices: Java संस्करण — virtual thread युग की practices
- C# से native DLL बुलाना: C++/CLI wrapper बनाम P/Invoke
- Shared memory के pitfalls और practical best practices
- COM STA/MTA मूल बातें — threading model और hang से कैसे बचें
- Windows I/O की गहराई (भाग 2) — Synchronous और async I/O: OVERLAPPED का वास्तविक अर्थ
संबंधित परामर्श क्षेत्र
KomuraSoft LLC C++ app और DLL की multithread design review, «कभी-कभी crash» या «केवल release build में गलत व्यवहार» जैसी race-condition bugs की root-cause investigation (dump analysis), और पुराने threaded code को आधुनिक C++ में migrate करने का consulting सँभालता है।
संदर्भ लिंक
-
ISO C++, C++ Core Guidelines - CP: Concurrency and parallelism. इस पर कि CP.1 (मानें कि आपका code multithreaded program के भाग के रूप में चलेगा) और CP.2 (data races से बचें) concurrency और parallelism अध्याय के आरंभिक नियम हैं; इस पर कि data race होने पर कोई guarantee नहीं रहती; और इस पर कि concurrent code के design नियम — lock कितने दायरे में पकड़े जाएँ, RAII का इस्तेमाल आदि — वहाँ व्यवस्थित हैं। ↩ ↩2
-
cppreference.com, std::jthread. इस पर कि C++20 का jthread std::thread से इस बात में भिन्न है कि उसका destructor स्वतः request_stop() बुलाता है फिर join करता है; इस पर कि thread function के अग्र argument के रूप में std::stop_token ले सकता है; और इस पर कि इससे exception फेंके जाने पर भी join और stop request दोनों guarantee होते हैं। ↩ ↩2
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. इस पर कि P0660R10 (<stop_token> और jthread) तथा P1135R6 (C++20 synchronization library) Visual Studio 2019 16.9 से supported हैं; और C++ standard library सुविधाओं की संस्करणानुसार support स्थिति पर। ↩ ↩2 ↩3
-
Microsoft Learn, scoped_lock Class. इस पर कि C++17 का scoped_lock construction पर एक या अधिक mutexes लेता है और destructor में छोड़ता है; कई mutexes साथ दिए जाएँ तो std::lock-equivalent deadlock-avoidance algorithm से लिए जाते हैं; exception फेंके जाने पर भी विश्वसनीय रूप से छोड़ता है; और एकल mutex हो तो lock_guard/unique_lock भी विकल्प हैं। ↩ ↩2 ↩3
-
Microsoft Learn, <atomic>. इस पर कि atomic operations indivisible हैं, इसलिए अन्य threads केवल operation से पहले या बाद की state देख सकते हैं; memory_order argument के आधार पर अन्य atomic operations की visibility पर ordering requirements स्थापित करना और उन्हें तोड़ने वाले compiler optimizations दबाना; atomic_flag हमेशा lock-free होना; और /clr:pure के तहत यह header blocked होना। ↩ ↩2
-
Microsoft Learn, <condition_variable>. इस पर कि condition variable पर wait के लिए mutex चाहिए, wait के दौरान lock छूटा रहता है; spurious wakeups हैं — सूचना बिना जागना — इसलिए wait पक्ष को लौटने पर शर्त स्पष्ट फिर जाँचनी चाहिए, और predicate रूप wait(lock, pred) वह loop आपके लिए चलाता है; तथा condition_variable_any किसी भी mutex type से जुड़ सकता है। ↩ ↩2
-
Microsoft Learn, <future>. इस पर कि future और shared_future के destructors नियमतः block नहीं करते, एकमात्र अपवाद यह कि std::async से शुरू work से बँधा future (या अंतिम shared_future) work अधूरा रहते destructor चलने पर shared state ready होने तक block करता है — व्यवहार जो standard में स्पष्ट note है। ↩ ↩2
-
Microsoft Learn, About Synchronization. Win32 synchronization primitives चुनने के guidance पर: portability-first C++ code के लिए std::mutex / std::shared_mutex और RAII सुझाए गए; Win32 wait API या cross-process synchronization चाहिए तब Win32 synchronization objects; नए intra-process code का default SRW lock, CRITICAL_SECTION recursive लेने के लिए सुरक्षित; और intra-process synchronization के लिए Mutex इस्तेमाल «आम गलती» है क्योंकि हमेशा kernel transition होता है। ↩ ↩2 ↩3
-
cppreference.com, std::thread::~thread. इस पर कि std::thread का destructor std::terminate बुलाता है यदि thread अभी joinable रहते (न join न detach) बुलाया जाए — अर्थात thread object नष्ट होने से पहले join या detach का निर्णय बिना exception पूरा होना चाहिए। ↩
-
Microsoft Learn, Best Practices in the Parallel Patterns Library. इस पर कि parallelism आदर्श रूप से यथासंभव ऊँचे स्तर (बाहरी loop) पर व्यक्त हो; छोटी या unbalanced per-iteration वाले parallel loops में fork/join scheduling overhead parallel execution के लाभ से बढ़ सकता है; और processor संख्या बढ़ने पर वह प्रवृत्ति मज़बूत होती है। ↩
-
Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. इस पर कि C++17 parallel algorithms library पूर्ण है, जबकि «पूर्ण» का अर्थ हर algorithm हर मामले में parallel होना नहीं; सबसे महत्वपूर्ण algorithms parallel करने और जो नहीं हैं उनके लिए भी execution-policy signatures देने की implementation नीति पर। ↩
-
Microsoft Learn, C++ standard library header files. इस पर कि multithreading-संबंधित standard headers <atomic> (C++11), <mutex> (C++11), <shared_mutex> (C++14), <condition_variable> (C++11), <future> (C++11), <stop_token> / <semaphore> / <latch> / <barrier> (C++20), और <thread> (C++11) के रूप में व्यवस्थित हैं। ↩
-
Microsoft Learn, Dynamic-Link Library Best Practices. इस पर कि DllMain loader lock पकड़े बुलाया जाता है, जो API बुला सकता है उस पर गंभीर बाधाएँ लगाता है; DllMain के भीतर दूसरे thread से synchronize करना deadlock कर सकता है; LoadLibrary बुलाना या thread खत्म होने की wait विशिष्ट निषिद्ध क्रियाएँ हैं; initialization यथासंभव delayed कर DllMain से बाहर ले जाना चाहिए; और loader lock को शीर्ष पर रखकर lock hierarchy परिभाषित करना चाहिए। ↩
-
Microsoft Learn, <thread>. इस पर कि <thread> header thread class और sleep_for जैसी helper functions परिभाषित करता है; /clr से compile code में यह header blocked है; और STDCPP_THREADS macro से thread support है या नहीं जाना जा सकता है। ↩
संबंधित लेख
निकटवर्ती विषयों में गहराई से जाने के लिए समान टैग वाले नवीनतम लेख।
Practical multithreading best practices: C संस्करण — Win32 API तरीके से सुरक्षित लिखना
Win32 के साथ C में multithreading का स्थापित तरीका _beginthreadex से thread बनाना, SRW lock और condition variable, Interlocked functions,...
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 ऐप विकास
व्यावसायिक ऐप, डिवाइस एकीकरण और संचार उपकरण, आवश्यकताओं से विकास तक।
अक्सर पूछे जाने वाले प्रश्न
इस लेख के विषय पर परामर्श में अक्सर पूछे जाने वाले प्रश्न।
- std::mutex और Win32 के CRITICAL_SECTION / SRW lock में कैसे चुनें?
- Portability को प्राथमिकता देने वाले सामान्य C++ code के लिए std::mutex / std::shared_mutex साथ RAII wrappers (lock_guard / scoped_lock) पहला विकल्प हैं। Win32 synchronization objects तब लें जब उन्हें WaitForMultipleObjects जैसी Win32 wait API से जोड़ना हो, या जब named object से cross-process synchronization चाहिए। Process के भीतर Win32 API सीधे इस्तेमाल कर रहे हों तो नए code का default SRW lock है, और CRITICAL_SECTION केवल तब जब उसी thread को उसे recursively लेना हो। Intra-process mutual exclusion के लिए Win32 Mutex इस्तेमाल करना classic गलती है, क्योंकि उसमें हमेशा kernel transition होता है और वह उसी अनुपात में धीमा है।
- std::thread का detach() इस्तेमाल करना ठीक है?
- नियम के रूप में, बचें। Detached thread join का हर साधन खो देता है, और process निकलते समय वह अभी चल रहा है या नहीं, उस पर नियंत्रण जाता रहता है। Classic accident: detached thread static variables या heap नष्ट होने के बाद भी चलता रहता है, और shutdown crash करता है। Thread के खत्म होने की wait कर पाना thread design की मूल requirement है, इसलिए jthread (जो स्वतः join करता है) इस्तेमाल करें, या thread हो तो code को scope खत्म होने से पहले हमेशा join करने की structure दें। detach केवल उस संकीर्ण स्थिति में स्वीकार्य है जहाँ thread process की नियति share कर सकता है और आप guarantee दे सकें कि वह shared state को छूता ही नहीं।
- C++ में threads के बीच synchronization के लिए volatile इस्तेमाल हो सकता है?
- नहीं। C++ का volatile उन reads-writes के लिए qualifier है जिन्हें आप compiler से optimize कर मिटवाना नहीं चाहते — उदाहरण के लिए memory-mapped I/O — और वह threads के बीच visibility या ordering की guarantee नहीं देता। कई threads बिना synchronization के एक ही variable पर पहुँचें तो वह data race है, और वह undefined behavior है। Threads के बीच shared flags और counters के लिए std::atomic इस्तेमाल करें, और कई variables एक साथ सुरक्षित करने हों तो std::mutex। std::atomic operation की indivisibility और memory_order पर आधारित ordering दोनों देता है।
- std::async सुविधाजनक लगता है, पर क्या pitfalls हैं?
- सबसे बड़ा pitfall future का destructor है। std::async से शुरू work से बँधा future (या अंतिम shared_future) work अधूरा रहते destructor चलने पर पूरा होने तक block करता है। लौटे future को पकड़े बिना फेंक दें तो वह वहीं synchronous execution के बराबर हो जाता है — accident जिसमें async जाना चाहा और sequential रह गए। साथ ही, launch policy न बताएँ तो काम वास्तव में अलग thread पर चलता है या नहीं, implementation के विवेक पर है। इस्तेमाल करें तो future का जीवन स्पष्ट रूप से सँभालें, और जहाँ concurrent execution guarantee चाहिए वहाँ std::launch::async निर्दिष्ट करें।