व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम प्रथाएँ: C++ संस्करण — RAII और jthread से संरचना द्वारा दुर्घटनाएँ मिटाना

· · Windows, मल्टीथ्रेडिंग, C++, Visual Studio, व्यावसायिक ऐप, बग जाँच, डिज़ाइन

«C# में ठीक चलने वाला डिज़ाइन C++ में पोर्ट करते ही कभी-कभी क्रैश करने लगा।» «हमने std::thread उपयोग किया, और अपवाद फेंके जाने पर पूरा ऐप terminate से तुरंत मर गया।» «हम volatile bool फ़्लैग से रोक रहे थे, पर केवल रिलीज़ बिल्ड में वह रुकता नहीं था।» — C++ मल्टीथ्रेडिंग वह ख़तरा लाती है जो प्रबंधित भाषाओं में बस नहीं है: डेटा रेस, जैसी है, अपरिभाषित व्यवहार (UB) है। बात केवल भ्रष्ट मान पढ़ने की नहीं; कंपाइलर की ऑप्टिमाइज़ेशन मान्यताएँ ढह जाती हैं, और आप ऐसी स्थिति में पहुँचते हैं जहाँ शाब्दिक रूप से कुछ भी हो सकता है।

यह लेख हमारी व्यावहारिक मल्टीथ्रेडिंग श्रृंखला का C++ संस्करण है। आधुनिक C++ (C++17/20) में व्यावसायिक ऐप, उपकरण नियंत्रण सॉफ़्टवेयर और DLL लिखने वाले डेवलपर्स के लिए, यह मल्टीथ्रेड डिज़ाइन के सामान्य सिद्धांतों — थ्रेड सीधे न जोड़ें, साझा परिवर्तनीय स्थिति घटाएँ, लॉक अनुशासन लागू करें, रुकने का तरीका सबसे पहले डिज़ाइन करें — को C++ और Windows के औज़ारों में उतारता है, C++-विशिष्ट जालों के साथ, अगस्त 2026 तक के प्राथमिक स्रोतों पर आधारित। इसे अकेले खड़ा रहने के लिए लिखा गया है। अन्य भाषाओं के लिए निकाले गए वही सिद्धांत «.NET संस्करण», «C संस्करण» और «Java संस्करण» में भी हैं।

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

  • C++ में डेटा रेस «भ्रष्ट मान पढ़ सकते हैं» नहीं — वह अपरिभाषित व्यवहार है। कोड में एक भी असिंक्रनाइज़्ड साझा परिवर्तनीय पहुँच न छोड़ना निरपेक्ष आवश्यकता है, अन्य भाषाओं से अधिक।1
  • std::thread नंगा उपयोग न करें। थ्रेड अभी joinable रहते std::thread का डिस्ट्रक्टर चले तो std::terminate प्रोसेस को तुरंत मार देता है। C++20 का std::jthread डिस्ट्रक्टर में स्वतः join करता है और उसमें रुकने का अनुरोध तंत्र (stop_token) बना होता है।23
  • लॉक हमेशा RAII से पकड़ें। हाथ से mtx.lock() लिखना बंद करें; lock_guard / scoped_lock उपयोग करें। अपवाद फेंके जाने पर भी डिस्ट्रक्टर लॉक विश्वसनीय रूप से छोड़ता है। कई लॉक एक साथ लेने पर scoped_lock डेडलॉक-परिहार एल्गोरिद्म से सँभालता है।4
  • volatile सिंक्रनाइज़ेशन का औज़ार नहीं। साझा फ़्लैग और काउंटर के लिए std::atomic, कई चर एक साथ सुरक्षित करने के लिए std::mutexstd::atomic अविभाज्यता और memory_order पर आधारित क्रम दोनों देता है।5
  • रendezvous प्रतीक्षा condition_variable के प्रेडिकेट-रूप wait से करें। कंडीशन वेरिएबल स्प्यूरियस वेकअप (सूचना बिना जागना) के अधीन हैं, इसलिए प्रेडिकेट के बिना wait बगों का अड्डा है।6
  • थ्रेड कैसे रोके, उसका मूल रूप jthread + stop_token (C++20) है। उससे पहले के परिवेश में std::atomic<bool> प्लस कंडीशन वेरिएबल से सहयोगी रोक हाथ से बनाएँ। थ्रेड का जबरन समापन C++ की दुनिया में बस अस्तित्वहीन मानें।3
  • std::async उपयोग से पहले जानें कि future का डिस्ट्रक्टर ब्लॉक कर सकता है। लौटा मान फेंक दें तो क्रमबद्ध निष्पादन जैसा ही प्रभाव मिलता है।7
  • Win32 सिंक्रनाइज़ेशन ऑब्जेक्ट केवल «Win32 प्रतीक्षा API के साथ काम» और «क्रॉस-प्रोसेस» परिदृश्यों के लिए जगह पाते हैं। बाकी जगह मानक लाइब्रेरी के विरुद्ध लिखना पोर्टेबिलिटी और रखरखाव के लिए बेहतर है।8

2. मल्टीथ्रेडिंग कठिन क्यों है — रेस कंडीशन, डेडलॉक और अपरिभाषित व्यवहार

संक्षेप में, मल्टीथ्रेडिंग जो समस्याएँ लाती है वे भाषा से निरपेक्ष दो प्रकार की हैं।

रेस कंडीशन वह बग है जिसमें परिणाम इस पर निर्भर करता है कि कई थ्रेड किसी कोड खंड तक किस क्रम में पहुँचते हैं। क्लासिक उदाहरण साझा काउंटर है: एक अभिव्यक्ति ++count मशीन-कोड स्तर पर तीन चरणों में टूटती है — पढ़ना, जोड़ना, वापस लिखना। दो थ्रेड उन तीन चरणों में एक साथ घुसें तो एक थ्रेड का जोड़ दूसरे की वापस-लेखन से ओवरराइट होकर खो जाता है। परिणाम हर चलान पर बदलता है, और कौन-सा परिणाम मिलेगा अप्रत्याशित है।

थ्रेड Bसाझा चर countथ्रेड Aथ्रेड Bसाझा चर countथ्रेड Acount = 10दो वृद्धि हुईं,फिर भी count = 11 — थ्रेड A का जोड़ खो गयापढ़ें (10)पढ़ें (10)स्थानीय जोड़ (11)स्थानीय जोड़ (11)वापस लिखें (11)वापस लिखें (11)

चित्र 1: क्लासिक रेस कंडीशन जिसमें साझा काउंटर पर वृद्धि खो जाती है। ++count के तीन चरणों के दौरान दूसरा थ्रेड बीच में आए तो जो वापस-लेखन अंतिम होता है वह दूसरे को ओवरराइट करता है

डेडलॉक वह अवस्था है जिसमें दो थ्रेड प्रत्येक उस लॉक की प्रतीक्षा करते हैं जो दूसरा पकड़े है, इसलिए कोई आगे नहीं बढ़ सकता। थ्रेड A लॉक 1 पकड़े लॉक 2 की प्रतीक्षा करता है; थ्रेड B लॉक 2 पकड़े लॉक 1 की प्रतीक्षा करता है — इतना ही दोनों को सदा के लिए रोकने को काफी है।

लॉक 2 लेने की प्रतीक्षालॉक 1 लेने की प्रतीक्षाथ्रेड Aलॉक 1 पकड़ेथ्रेड Bलॉक 2 पकड़े

चित्र 2: डेडलॉक की चक्रीय प्रतीक्षा। प्रतीक्षा के तीर जैसे ही वलय बनाते हैं, उस वलय का हर थ्रेड सदा रुक जाता है

दोनों को असुविधाजनक बनाने वाली बात समय-निर्भरता है। विकास मशीन पर दसियों हज़ार चलानों में एक बार लगने वाला अंतर्ग्रथन ग्राहक की मशीन पर, अलग कोर संख्या और अलग समय के साथ, हर दिन हो सकता है। «डिबगर लगे होने पर पुनरुत्पादित नहीं होता» और «लॉग जोड़ते ही गायब हो गया» दोनों इसलिए होते हैं क्योंकि स्वयं अवलोकन समय बदल देता है — यही रेस-बग का क्लासिक व्यवहार है। इसीलिए इस लेख का हर सिद्धांत एक दिशा की ओर है: सही सिंक्रनाइज़ करने की चिंता से पहले, सिंक्रनाइज़ेशन चाहिए वाले स्थान घटाएँ।

2.1. C++ में डेटा रेस सीधे अपरिभाषित व्यवहार है

उस पर C++ में एक और परत है जो अन्य भाषाओं में नहीं। C++ मानक के अनुसार, कई थ्रेड बिना सिंक्रनाइज़ेशन के एक ही मेमोरी स्थान पर पहुँचें और कम से कम एक लिखे, तो वह डेटा रेस है, और वह अपरिभाषित व्यवहार है। C++ Core Guidelines का समवर्ती अध्याय (CP.2, «Avoid data races») इसे सबसे पहली निरपेक्ष नियम के रूप में रखता है।1 अपरिभाषित व्यवहार «पुराना मान या नया पढ़ सकते हैं» जैसी नरम कहानी नहीं। कंपाइलर इस आधार पर ऑप्टिमाइज़ करता है कि डेटा रेस नहीं है, इसलिए स्रोत से कभी न अनुमानित व्यवहार — लूप से शर्त जाँच गायब होना, लेखन का पुनर्क्रम या विलय — वैध रूप से होता है। क्लासिक दुर्घटना जिसमें «volatile bool रोक फ़्लैग केवल रिलीज़ बिल्ड में विफल होता है» ठीक इसी का पाठ्यपुस्तक मामला है।

2.2. RAII नींव है

C++-विशिष्ट एक और आधार अपवाद और संसाधन प्रबंधन है। C++ में finally नहीं; उसके स्थान पर RAII (डिस्ट्रक्टर से स्वचालित मुक्ति) है, और मल्टीथ्रेडिंग के औज़ार इसी मान्यता पर डिज़ाइन हैं कि आप उसे उपयोग करेंगे। «लॉक ऑब्जेक्ट के जीवन से सँभालें»; «थ्रेड का join भी ऑब्जेक्ट के जीवन से गारंटी दें» — उस परंपरा पर चलना सुरक्षित मल्टीथ्रेडेड C++ लिखने की नींव है।

3. थ्रेड कैसे शुरू करें — thread का जाल, और jthread

3.1. std::thread का डिस्ट्रक्टर «दुर्घटना पैदा करने के लिए डिज़ाइन है»

std::thread का जाना-माना जाल है। थ्रेड अभी joinable रहते (न join न detach) डिस्ट्रक्टर चले तो std::terminate बुलाया जाता है और प्रोसेस तुरंत मर जाता है।9

void process()
{
    std::thread worker([]{ HeavyWork(); });
    DoSomething();      // ← यदि यहाँ अपवाद फेंका जाए...
    worker.join();      // ← join कभी नहीं पहुँचता; worker का डिस्ट्रक्टर terminate बुलाता है
}

इसे अपवाद-सुरक्षित बनाने के लिए try/catch से join गारंटी करनी पड़ती थी — RAII भाषा में विकृत स्थिति जहाँ केवल थ्रेड हाथ से सँभालने पड़ते थे। C++20 का std::jthread इसे सुलझाता है। क्योंकि उसका डिस्ट्रक्टर स्वतः रोक अनुरोध जारी करता है फिर join करता है, ऊपर का कोड std::jthread पर स्विच करते ही अपवाद-सुरक्षित हो जाता है।2 MSVC पर <stop_token> और jthread Visual Studio 2019 16.9 से उपलब्ध हैं।3

std::threadन join न detachstd::threadपहले ही joinstd::jthread - C++20थ्रेड शुरू हो चुका हैस्कोप निकलने परक्या होता है?std::terminateप्रोसेस तुरंत मरता हैसुरक्षित joinस्वचालित request_stop + joinअपवाद फेंके जाने पर भी सुरक्षित

चित्र 3: थ्रेड ऑब्जेक्ट का जीवन और वह कैसे समाप्त होता है। std::thread विनिर्देशित है कि join भूलें तो तुरंत मरे, इसलिए C++20 से jthread को डिफ़ॉल्ट बनाएँ

नियम के रूप में detach() उपयोग न करें। join का हर साधन खो चुका थ्रेड शटडाउन क्रैश का क्लासिक कारण बनता है, प्रोसेस निकलते समय स्थैतिक चर और हीप के विनाश से दौड़ता हुआ।

3.2. «थ्रेड स्तर से ऊपर» के औज़ार — async, future और समांतर एल्गोरिद्म

.NET संस्करण का सिद्धांत «थ्रेड स्वयं न बनाएँ» C++ में इन औज़ारों पर पड़ता है।

  • std::async + std::future: एकबारगी अतुल्यकालिक कार्य और उसका परिणाम पाने के लिए। पर महत्वपूर्ण विशेषता: std::async से शुरू कार्य से बँधा future (या अंतिम shared_future) कार्य अधूरा रहते डिस्ट्रक्टर चलने पर पूरा होने तक ब्लॉक करता है7 वास्तव में std::launch::async से शुरू काम के लिए लौटा future फेंकना उस बिंदु पर तुल्यकालिक निष्पादन के बराबर है। बदतर, लॉन्च नीति न बताएँ तो कार्यान्वयन डिफ़ॉल्ट रूप से deferred (आलसी निष्पादन) चुन सकता है, जिसमें get() / wait() कोई न बुलाए तो काम चलता ही नहीं और चुपचाप गायब हो जाता है। समवर्ती निष्पादन गारंटी चाहिए तो std::launch::async स्पष्ट करें, और मालिक future का जीवन सँभाले।
  • PPL - Parallel Patterns Library - concurrency::parallel_for / parallel_for_each: संग्रह के हर तत्व पर काम समांतर लागू करना। पर एक पुनरावृत्ति का काम बहुत छोटा हो तो fork/join ओवरहेड लाभ खा जाता है, इसलिए नियमतः बाहरी लूप पर समांतर करें।10
  • C++17 के समांतर एल्गोरिद्म - std::execution::par: MSVC पर प्रमुख एल्गोरिद्म समांतर हैं (सभी नहीं)।11 ध्यान दें कि निष्पादन नीति के तहत तत्व प्रसंस्करण से अपवाद निकले तो std::terminate बुलाया जाता है। कॉलबैक के भीतर अपना अपवाद सीमा (try/catch) रखना खंड 6 की थ्रेड सीमा जैसी ही सोच है।

अन्यत्र खींची रेखा — «I/O की प्रतीक्षा थ्रेड जोड़कर हल नहीं होती» — बिना परिवर्तन लागू रहती है। नेटिव Windows कोड के लिए OVERLAPPED I/O और IOCP वे औज़ार हैं जो वह काम पकड़ते हैं (यांत्रिकी के लिए देखें «Windows I/O की गहराई, भाग 2»)।

4. साझा परिवर्तनीय स्थिति घटाना — विभाजन, मान से पास, const और कतारें

प्रतिस्पर्धा तभी उठती है जब «कई थ्रेड» और «साझा परिवर्तनीय डेटा» दोनों हों। थ्रेडों की संख्या आवश्यकताएँ तय करती हैं, इसलिए डिज़ाइन काट सकता है साझा करना। साधन तीन परिवारों में पड़ते हैं — विभाजन, अपरिवर्तनीय बनाना, और डेटा सौंपना — और C++ में प्रत्येक ऐसे लिखते हैं।

विभाजित करें। समांतर एकत्रीकरण जैसे काम में हर थ्रेड साझा योग में लिखने के बजाय हर थ्रेड को अपना स्थानीय उपयोग दें और अंत में एक बार मिलाएँ। साझा मान पर लेखन «हर पुनरावृत्ति» से «प्रति थ्रेड एक बार» तक गिरता है, सिंक्रनाइज़ेशन लागत और प्रतिस्पर्धा खिड़की दोनों को परिमाणों में काटता है। वह एक मेल चरण std::mutex से या std::atomic पर fetch_add से — दोनों ठीक।

मान से पास करें। थ्रेड को चाहिए डेटा आरंभ पर कॉपी (या मूव) से दें तो वह डेटा थ्रेड का अनन्य हो जाता है, और सिंक्रनाइज़ेशन नहीं चाहिए। लैम्ब्डा को संदर्भ से पकड़ना ([&]) फिर समाप्त जीवन वाले चर को छूना आम दुर्घटना है, इसलिए थ्रेडों को दी लैम्ब्डा स्पष्ट कैप्चर, नियमतः कॉपी या मूव उपयोग करें। हाँ, «कॉपी हुआ, इसलिए अनन्य» तभी जब मान पॉइंटर या shared_ptr जैसे उपनाम रहित गहरा मान-ग्राफ हो। कच्चे पॉइंटर वाली संरचना कॉपी करने पर भी वह जिस ओर इशारा करता है साझा रहता है।

const के रूप में साझा करें। केवल पढ़े जाने वाले डेटा किसी भी संख्या के थ्रेड से एक साथ पढ़ना सुरक्षित है। कॉन्फ़िग मान, मास्टर डेटा, गणना इनपुट आदि निर्माण के बाद न लिखे जाने वाले const साझा (std::shared_ptr<const Config> जैसे) बनाएँ तो बिना सिंक्रनाइज़ेशन साझा हो सकते हैं। एक चेतावनी: shared_ptr<const T> जो मना करता है वह केवल उस हैंडल से परिवर्तन है। कहीं और गैर-const उपनाम बचा हो, या mutable सदस्य फिर लिखा जाए, प्रतिस्पर्धा रहती है — इसलिए वह भी डिज़ाइन करें, «निर्माण खत्म होते ही गैर-const संदर्भ छोड़ें और बाद में कोई न लिखे» तक। केवल यह तय करना कि «बदलाव चाहिए तो जगह पर बदलने के बजाय नया ऑब्जेक्ट बनाकर बदलें» एक परिवर्तनीय स्थिति हटाता है जिसे अन्यथा सुरक्षित रखना पड़ता (स्वयं स्वैप के जीवन प्रबंधन के लिए खंड 5.2 की चेतावनी देखें)।

कतार से सौंपें। थ्रेडों के बीच डेटा प्रवाह साझा चर के बजाय उत्पादक/उपभोक्ता कतार से चलाएँ। C++ मानक में चैनल प्रकार नहीं, इसलिए std::mutex + std::condition_variable से छोटी कतार लिखना स्थापित पैटर्न है।

template <typename T>
class BlockingQueue {
public:
    explicit BlockingQueue(std::size_t capacity) : capacity_(capacity)
    {
        if (capacity == 0)                          // क्षमता 0 वह जाल है जहाँ हर Push सदा प्रतीक्षा करता है
            throw std::invalid_argument("capacity must be positive");
    }

    // भरा हो तो जगह (या रोक अनुरोध) तक प्रतीक्षा। false मतलब रोक अनुरोध।
    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;                       // रोक अनुरोध से जगाया गया
            if (st.stop_requested())                // जगह और रोक साथ हों तो रोक प्राथमिक,
                return false;                       // और रुकना शुरू होने के बाद धक्का अस्वीकार
            queue_.push(std::move(item));
        }
        not_empty_.notify_one();   // लॉक के बाहर सूचित करें
        return true;
    }

    // रोक अनुरोध (stop_token) या आइटम आने तक प्रतीक्षा। रुके पर 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;                // रोक अनुरोध से जगाया गया
            if (st.stop_requested())                // आइटम और रोक साथ हों तो रोक प्राथमिक,
                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-जागरूक wait के लिए
    std::condition_variable_any not_full_;
    std::queue<T> queue_;
};

यहाँ दो डिज़ाइन बिंदु हैं। पहला, क्षमता सीमित करें और भरा होने पर उत्पादक पक्ष को प्रतीक्षा कराएँ। ऊपरी सीमा रहित कतार उन व्यवस्थाओं में टाइम बम बनती है जहाँ उत्पादन खपत से आगे निकलता है: वह «चलता रहता है», पर मेमोरी बढ़ती जाती है। भरा होने पर Push का ब्लॉक प्राकृतिक बैकप्रेशर बनता है, ओवरलोड को यंत्रवत ऊपर भेजता है। दूसरा, कंडीशन वेरिएबल स्प्यूरियस वेकअप (सूचना बिना जागना) के अधीन हैं, इसलिए wait हमेशा प्रेडिकेट से बुलाएँ। wait का प्रेडिकेट रूप भीतर «शर्त सत्य होने तक लूप» तर्क आपके लिए चलाता है।6

5. लॉक अनुशासन — RAII और scoped_lock

साझा परिवर्तनीय स्थिति घटाने के बाद भी अक्सर शून्य तक नहीं पहुँचते। जो साझा बचा उसके लिए बहिष्करण उपयोग करें, पर बिना अनुशासन लॉक केवल प्रतिस्पर्धा छिपाता है।

पहले, लॉक की इकाई «कोड का टुकड़ा» नहीं «डेटा» सोचें। सुरक्षित करने वाले हर परिवर्तनीय डेटा समूह को एक म्यूटेक्स दें (उसे private सदस्य बनाएँ, बाहर न खोलें), और उस डेटा को छूने वाले हर स्थान पर वही म्यूटेक्स लें — इस तालिका का टूटा रूप ही अधिकांश रेस बग वास्तव में हैं। और लॉक पकड़े केवल वही कर सकते हैं जो वह डेटा पढ़ना-लिखना है। लॉक पकड़े फ़ाइल I/O, नेटवर्क कॉल और कॉलबैक (बाहरी कोड में कॉल) न केवल पकड़ बढ़ाते हैं — वे रास्ता खोलते हैं जहाँ कॉल किया गया दूसरा लॉक लेने की कोशिश कर डेडलॉक करता है। लॉक के बाहर तैयार करें, और लॉक के भीतर केवल बदलें मूल रूप है।

5.1. हाथ से lock()/unlock() वर्जित

std::mutex के lock() / unlock() सीधे बुलाने वाला कोड अपवाद या जल्दी लौटने पर लॉक नहीं छोड़ पाता। लॉक लेना और छोड़ना हमेशा RAII रैपर पर छोड़ें।

रैपर उपयोग
std::lock_guard एक म्यूटेक्स ठीक एक स्कोप भर पकड़े — सबसे मूल रूप
std::scoped_lock (C++17) कई म्यूटेक्स एक साथ लेता है। क्रम समस्या डेडलॉक-परिहार एल्गोरिद्म से सुलझाता है4
std::unique_lock बीच में खोलकर फिर बंद करना हो, या condition_variable::wait को देना हो

दो या अधिक लॉक हों तो थ्रेड के अनुसार लेने का क्रम पलटना क्लासिक डेडलॉक पैटर्न है (चित्र 2 की चक्रीय प्रतीक्षा ठीक ऐसे जन्म लेती है)। सुधार नियम बनाना कि «हर थ्रेड लॉक एक ही क्रम में ले», पर जब उन्हें एक ही समय ले रहे हों, C++ का बेहतर उत्तर है: कई म्यूटेक्स std::scoped_lock को साथ दें और लाइब्रेरी डेडलॉक-मुक्त लेने का क्रम गारंटी करती है4 दो ऑब्जेक्ट के बीच स्थानांतरण जैसे «दोनों लॉक» चाहिए स्थितियों में उन्हें अलग-अलग कभी न लें — हमेशा साथ लें।

void Transfer(Account& from, Account& to, int amount)
{
    if (&from == &to) return;                  // उसी खाते के लिए कुछ न करें (नीचे टिप्पणी देखें)
    std::scoped_lock lock(from.mtx, to.mtx);   // दोनों साथ; क्रम लाइब्रेरी सुलझाती है
    from.balance -= amount;
    to.balance   += amount;
}

ऊपर पहचान जाँच सजावट नहीं। वही Account from और to दोनों बने तो आप एक ही गैर-पुनरावर्ती म्यूटेक्स scoped_lock को दो बार देते हैं, जिससे हैंग या अपरिभाषित व्यवहार होता है। «दोनों लॉक» करने वाले हर फ़ंक्शन पर वही-ऑब्जेक्ट बहिष्करण हमेशा लगाएँ।

«अक्सर पढ़े, कम लिखे» डेटा के लिए std::shared_mutex (C++17) पढ़ने/लिखने लॉक के रूप में उपयोग हो सकता है।12 recursive_mutex वह प्रकार है जो «उसी थ्रेड का फिर लेना न टूटे» के लिए डिज़ाइन है, पर पुनरावर्ती लेने वाला डिज़ाइन अक्सर संकेत है कि लॉक की ज़िम्मेदारी सीमा धुँधली हो गई — पहले संरचना फिर देखें।

5.2. atomic की सही भूमिका

std::atomic एक चर पर अणु ऑपरेशन देता है, प्लस memory_order पर आधारित क्रम।5 उसकी जगह .NET संस्करण के Interlocked जैसी स्थितियों में है: एक चर अद्यतन करना, जैसे काउंटर या फ़्लैग। वह कई चर एक साथ सुसंगत नहीं रख सकता, इसलिए उसके लिए std::mutex पर लौटें।

कच्चे पॉइंटर की अदला-बदली (std::atomic<T*>) का अपना जाल है। अदला-बदली स्वयं अणु होने पर भी, पुराने ऑब्जेक्ट का जीवन बदले जाने के बाद कोई सुरक्षित नहीं करता। लेखक बदलकर delete करे ठीक पहले पाठक पुराना पॉइंटर लोड करे तो मुक्त मेमोरी पर पहुँच मिलती है। C++ में «अपरिवर्तनीय ऑब्जेक्ट बदलकर साझा करें» डिज़ाइन करना हो तो वह साधन चुनें जो जीवन प्रबंधन के साथ आता है — लॉक-सुरक्षित std::shared_ptr<const T> बदलना, या C++20 का std::atomic<std::shared_ptr<T>>

और दोहराना: volatile थ्रेड-सिंक्रनाइज़ेशन का औज़ार नहीं। स्वयं memory_order बताने वाला लॉक-मुक्त प्रोग्रामिंग विशेषज्ञ क्षेत्र है, जिसमें डिफ़ॉल्ट (seq_cst) से ढीला करने का वैध कारण और सही करने की जाँच दोनों चाहिए। व्यावसायिक ऐप में या तो डिफ़ॉल्ट रखें या शुरू से mutex से लिखें।

6. रुकने का डिज़ाइन — stop_token और सहयोगी रोक

मल्टीथ्रेड डिज़ाइन की समीक्षा में पहला प्रश्न «यह कैसे रुकता है?» है। और C++ में थ्रेड को बाहर से सुरक्षित रोकने का साधन नहीं (Win32 का TerminateThread कितना ख़तरनाक है C संस्करण में विस्तार से है)। इसलिए थ्रेड कैसे रुकता है C++ के औज़ारों से सहयोगी रोक के इर्द-गिर्द बनाना पड़ता है — रोकने वाला पक्ष केवल अनुरोध जारी करता है; थ्रेड स्वयं तय करता है कब और कैसे खत्म हो, उस बिंदु पर जो चीज़ें साफ़ छोड़ता है; और join पूरा होना ही «रुक गया» गिना जाता है।

C++20 में std::jthread में रोक तंत्र बना है। request_stop() बुलाने से थ्रेड फ़ंक्शन को मिला std::stop_token पर रोक अनुरोध उठता है, और लूप उसे जाँचता है। condition_variable_any का wait stop_token सीधे ले सकता है, इसलिए «काम आने की प्रतीक्षा करता थ्रेड» भी रोक अनुरोध से तुरंत जगाया जा सकता है (खंड 4 का BlockingQueue::Pop ठीक यही रूप है)।

class Worker {
public:
    void Start()
    {
        if (thread_.joinable())                       // पहले से चलते दोहरा Start अस्वीकार करें।
            throw std::logic_error("already running"); // अस्वीकार के बजाय असाइन करें तो नया
                                                       // थ्रेड चलने लगेगा, और पुराने के रुकने
                                                       // की प्रतीक्षा करते दो वर्कर
                                                       // साथ-साथ चलेंगे
        thread_ = std::jthread([this](std::stop_token st) {
            try {
                while (!st.stop_requested()) {
                    if (auto item = queue_.Pop(st)) {   // रोक अनुरोध पर भी जागता है
                        try {
                            Process(*item, st);          // भीतर ब्लॉक कर सकने वाले काम में भी st दें
                        } catch (...) {
                            ReportError(std::current_exception());  // एक विफलता दर्ज कर जारी रखें
                        }
                    }
                }
            } catch (...) {
                // थ्रेड सीमा की अंतिम रक्षा रेखा (Pop या मूव की विफलता भी पकड़ती है)।
                // यहाँ से अपवाद निकले तो std::terminate पूरी प्रोसेस गिराता है,
                // इसलिए ReportError स्वयं कभी न फेंके
                ReportError(std::current_exception());
            }
        });
    }
    // स्पष्ट Stop नहीं चाहिए:
    // Worker का डिस्ट्रक्टर -> jthread का डिस्ट्रक्टर -> request_stop() + join()
private:
    BlockingQueue<WorkItem> queue_{100};   // क्षमता-सीमित (खंड 4)
    std::jthread thread_;
};
रोक अनुरोधरोकने वाला पक्ष- jthread का डिस्ट्रक्टर, या request_stopstop_tokenगणना लूप:stop_requested() जाँचता हैप्रतीक्षारत थ्रेड:condition_variable_any::wait(lock, st, pred)तुरंत जागता हैसाफ़ कर स्वयं लौटता हैjoin rendezvous पूरा करता हैअब ही रुक गया कह सकते हैं

चित्र 4: C++20 सहयोगी रोक। रोकने वाला पक्ष केवल अनुरोध जारी करता है; थ्रेड स्वयं तय करता है कैसे खत्म हो; join पूरा होना ही रुका माना जाता है

एक और बात: वर्कर के भीतर try/catch छोड़ा नहीं जा सकता। jthread जो अपवाद-सुरक्षित बनाता है वह join, और केवल join है — थ्रेड फ़ंक्शन से अपवाद निकले तो std::terminate प्रोसेस गिराता है, ठीक std::thread की तरह। थ्रेड सीमा पर स्पष्ट तय करें कि एक काम की विफलता कैसे सँभालें (दर्ज कर जारी रखें, या त्रुटि चैनल से मालिक को बताएँ)।

उसी कारण ध्यान दें कि Process को भी stop_token दिया जाता है। एक काम का प्रसंस्करण भीतर ब्लॉक करे (नेटवर्क प्रतीक्षा, लंबी गणना आदि) और वह बिंदु रोक अनुरोध न देख सके, तो डिस्ट्रक्टर का निहित join उस एक आइटम के खत्म होने की सदा प्रतीक्षा करेगा। सहयोगी रोक तभी टिकती है जब टोकन हर प्रतीक्षा स्थान तक पहुँच चुका हो। काम में बाहरी कॉल हो जो बाधित न हो सके, तो समय-सीमा लगाएँ और एक आइटम कितनी देर चल सकता है उसकी ऊपरी सीमा रखें।

C++17 से पहले के परिवेश में वही रूप std::atomic<bool> रोक फ़्लैग प्लस condition_variable के notify_all से हाथ से बनाते हैं। यहाँ मुख्य बात रोक-फ़्लैग जाँच को कंडीशन वेरिएबल के प्रेडिकेट में मोड़ना है — केवल फ़्लैग उठाएँ और सूचित करना भूलें तो प्रतीक्षारत थ्रेड कभी नहीं जागेगा।

7. Windows-विशिष्ट चिंताएँ — Win32 API की सीमा

7.1. मानक लाइब्रेरी और Win32 सिंक्रनाइज़ेशन ऑब्जेक्ट के बीच चुनाव

Microsoft दस्तावेज़ पोर्टेबिलिटी-प्राथमिक C++ कोड के लिए std::mutex / std::shared_mutex सुझाता है, और Win32 सिंक्रनाइज़ेशन ऑब्जेक्ट की जगह «जब Win32 प्रतीक्षा API चाहिए» और «क्रॉस-प्रोसेस सिंक्रनाइज़ेशन» बताता है।8

स्थिति चुनाव
सामान्य इंट्रा-प्रोसेस बहिष्करण std::mutex + RAII (डिफ़ॉल्ट)
बहुत पढ़ना, कम लिखना std::shared_mutex
WaitForMultipleObjects से कई ऑब्जेक्ट एक साथ प्रतीक्षा इवेंट और म्यूटेक्स जैसे Win32 कर्नेल ऑब्जेक्ट
क्रॉस-प्रोसेस बहिष्करण / सूचना नामित म्यूटेक्स, इवेंट, सेमाफोर
Win32 API सीधे उपयोग कर इंट्रा-प्रोसेस लॉक SRW लॉक (पुनरावृत्ति चाहिए तभी CRITICAL_SECTION)8

प्रोसेसों के आर-पार साझा मेमोरी पहुँच बहिष्कृत करने के ठोस डिज़ाइन के लिए देखें «साझा मेमोरी के जाल और व्यावहारिक सर्वोत्तम प्रथाएँ»।

7.2. DllMain के भीतर थ्रेड न छुएँ

DLL लिखते समय गंभीर बाधा लोडर लॉक है। DllMain लोडर लॉक पकड़े बुलाया जाता है, इसलिए उसके भीतर दूसरे थ्रेड से सिंक्रनाइज़ करना, थ्रेड खत्म होने की प्रतीक्षा, या LoadLibrary बुलाना डेडलॉक या अप्रत्याशित व्यवहार पैदा करता है। थ्रेड शुरू या join करने वाला कोई आरंभीकरण DllMain से बाहर, स्पष्ट आरंभीकरण फ़ंक्शन में ले जाएँ।13

7.3. UI थ्रेड और COM अपार्टमेंट

Windows डेस्कटॉप ऐप की एक मज़बूत बाधा भाषा से निरपेक्ष लागू होती है: केवल वह थ्रेड जिसने विंडो या नियंत्रण बनाया — UI थ्रेड — उसे छू सकता है। Windows विंडो संदेश उस थ्रेड की संदेश कतार में पहुँचाता है जिसने वह विंडो बनाई, इसलिए UI का निर्माण और संचालन उसी थ्रेड पर केंद्रित होना चाहिए। वर्कर थ्रेड से स्क्रीन अद्यतन करना हो तो सीधे न छुएँ — PostMessage (अतुल्यकालिक) से UI थ्रेड से कहें, और UI थ्रेड की विंडो प्रक्रिया में सँभालें। तुल्यकालिक रूप SendMessage बुलाना, जबकि UI थ्रेड उस वर्कर के खत्म होने की प्रतीक्षा कर रहा हो, डेडलॉक पैदा करता है जिसमें प्रत्येक दूसरे की प्रतीक्षा करता है, इसलिए वर्कर से सूचनाओं के लिए अतुल्यकालिक रूप डिफ़ॉल्ट बनाएँ। STA/MTA, जहाँ COM शामिल है, «COM STA/MTA मूल बातें - थ्रेडिंग मॉडल और हैंग से कैसे बचें» में है। यह भी ध्यान दें कि /clr से कंपाइल C++/CLI कोड में <thread> और <mutex> जैसे मानक थ्रेड हेडर अवरुद्ध हैं।14

8. सत्यापन और डिबग — इस मान्यता पर तैयारी कि पुनरुत्पादित नहीं होगा

रेस बग खोजने के लिए परीक्षण पर भरोसा नहीं कर सकते, क्योंकि सामान्य परीक्षण «संयोग से रेस न हुआ» चलान को पास गिनता है। तैयारी तीन परतों में सोचें।

पहली रक्षा रेखा अब तक के डिज़ाइन सिद्धांत हैं, ठीक जैसे हैं। समीक्षा में तालिका से पुष्टि करें: कौन सा परिवर्तनीय डेटा साझा है, कौन सा म्यूटेक्स प्रत्येक टुकड़ा सुरक्षित करता है, कई लॉक लेने का क्रम अद्वितीय है या नहीं (या scoped_lock से साथ लिए गए), और रोक पथ कहाँ है। वह डिज़ाइन जिसके लिए यह तालिका न लिख सकें अधूरा है, चाहे अभी कितना अच्छा चले।

दूसरा, असामान्य अवस्थाएँ छिपाने के बजाय अवलोकनीय बनाएँ। जो लॉक कभी विफल न होना चाहिए उस पर timed_mutex का try_lock_for या condition_variable का wait_for से समय-सीमा लगाएँ, और समय-समाप्ति को विसंगति के रूप में लॉग करें — वह शाश्वत हैंग को पता लगाने योग्य विफलता बनाता है। थ्रेड सीमा के try/catch (खंड 6) में पकड़े अपवाद हमेशा लॉग करें। मैदान में हैंग या क्रैश हो तो डंप लें, हर थ्रेड का स्टैक जाँचें, और देखें कि उनकी लॉक प्रतीक्षा चक्र तो नहीं बनाती। डंप और लॉगिंग की व्यवस्था «Windows ऐप क्रैश के लिए लॉगिंग और डंप कैप्चर डिज़ाइन» में है।

तीसरा, भार के नीचे हिलाएँ। कोर से अधिक समांतरता से लंबे समय चलाना, प्रसंस्करण क्रम यादृच्छिक करना, और कृत्रिम विलंब डालना व्यावहारिक तनाव-परीक्षण तकनीकें हैं जो विकास मशीन पर रेस «जैकपॉट» लगाना आसान बनाती हैं। डिबग बिल्ड में गायब बग अक्सर अनुकूलित रिलीज़ बिल्ड में भारी भार के नीचे आसानी से पुनरुत्पादित होते हैं।

9. सार — C++ जाँच सूची

हर भाषा के साझा सिद्धांतों — थ्रेड सीधे न बनाएँ, साझा परिवर्तनीय स्थिति न्यूनतम करें, लॉक और डेटा का एक-से-एक मेल, सहयोगी रोक — पर C++-विशिष्ट जाँचें चढ़ाएँ।

  1. क्या std::thread नंगा उपयोग हो रहा है (क्या jthread हो सकता है? अपवाद पथ पर भी join गारंटी है?)
  2. क्या detach() उपयोग नहीं हो रहा?
  3. क्या लैम्ब्डा कैप्चर स्पष्ट हैं, और संदर्भ-कैप्चर किया चर थ्रेड से अधिक जीता है?
  4. क्या विश्वास से कह सकते हैं कि कहीं एक भी असिंक्रनाइज़्ड साझा परिवर्तनीय पहुँच (= अपरिभाषित व्यवहार) नहीं?
  5. क्या हाथ से lock() / unlock() नहीं, और कई लॉक scoped_lock से साथ लिए जाते हैं?
  6. क्या हर condition_variable::wait प्रेडिकेट के साथ है?
  7. क्या साझा फ़्लैग के लिए volatile नहीं (उसके स्थान पर std::atomic है)?
  8. क्या रोक पथ stop_token (या atomic फ़्लैग प्लस सूचना) के इर्द-गिर्द डिज़ाइन है, join पूरा होना rendezvous की पुष्टि करता है?
  9. क्या std::async का future नहीं फेंका जा रहा?
  10. क्या DllMain थ्रेड शुरू, सिंक्रनाइज़ या join से मुक्त है?

मल्टीथ्रेडेड C++ अपरिभाषित व्यवहार के किनारे के ठीक साथ चलने वाला काम है, पर उसे पलटें तो मतलब है कि RAII और मानक लाइब्रेरी की परंपराओं के साथ ईमानदारी से चलना उस किनारे से वास्तविक दूरी रखता है। jthread, scoped_lock, प्रेडिकेट-रूप wait, atomic — इन औज़ारों में सही डिफ़ॉल्ट चुनना, C++ में, डिज़ाइन सिद्धांतों का अभ्यास स्वयं है।

संबंधित लेख

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

KomuraSoft LLC C++ ऐप और DLL की मल्टीथ्रेड डिज़ाइन समीक्षा, «कभी-कभी क्रैश» या «केवल रिलीज़ बिल्ड में गलत व्यवहार» जैसी रेस-कंडीशन बगों की मूल-कारण जाँच (डंप विश्लेषण), और पुराने थ्रेडेड कोड को आधुनिक C++ में माइग्रेट करने का परामर्श सँभालता है।

संदर्भ लिंक

  1. ISO C++, C++ Core Guidelines - CP: Concurrency and parallelism. इस पर कि CP.1 (मानें कि आपका कोड मल्टीथ्रेडेड प्रोग्राम के भाग के रूप में चलेगा) और CP.2 (डेटा रेस से बचें) समवर्तीता और समांतरता अध्याय के आरंभिक नियम हैं; इस पर कि डेटा रेस होने पर कोई गारंटी नहीं रहती; और इस पर कि समवर्ती कोड के डिज़ाइन नियम — लॉक कितने दायरे में पकड़े जाएँ, RAII का उपयोग आदि — वहाँ व्यवस्थित हैं।  2

  2. cppreference.com, std::jthread. इस पर कि C++20 का jthread std::thread से इस बात में भिन्न है कि उसका डिस्ट्रक्टर स्वतः request_stop() बुलाता है फिर join करता है; इस पर कि थ्रेड फ़ंक्शन के अग्र तर्क के रूप में std::stop_token ले सकता है; और इस पर कि इससे अपवाद फेंके जाने पर भी join और रोक अनुरोध दोनों गारंटी होते हैं।  2

  3. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. इस पर कि P0660R10 (<stop_token> और jthread) तथा P1135R6 (C++20 सिंक्रनाइज़ेशन लाइब्रेरी) Visual Studio 2019 16.9 से समर्थित हैं; और C++ मानक लाइब्रेरी सुविधाओं की संस्करणानुसार समर्थन स्थिति पर।  2 3

  4. Microsoft Learn, scoped_lock Class. इस पर कि C++17 का scoped_lock निर्माण पर एक या अधिक म्यूटेक्स लेता है और डिस्ट्रक्टर में छोड़ता है; कई म्यूटेक्स साथ दिए जाएँ तो std::lock-समकक्ष डेडलॉक-परिहार एल्गोरिद्म से लिए जाते हैं; अपवाद फेंके जाने पर भी विश्वसनीय रूप से छोड़ता है; और एकल म्यूटेक्स हो तो lock_guard/unique_lock भी विकल्प हैं।  2 3

  5. Microsoft Learn, <atomic>. इस पर कि अणु ऑपरेशन अविभाज्य हैं, इसलिए अन्य थ्रेड केवल ऑपरेशन से पहले या बाद की अवस्था देख सकते हैं; memory_order तर्क के आधार पर अन्य अणु ऑपरेशन की दृश्यता पर क्रम आवश्यकताएँ स्थापित करना और उन्हें तोड़ने वाले कंपाइलर ऑप्टिमाइज़ेशन दबाना; atomic_flag हमेशा लॉक-मुक्त होना; और /clr:pure के तहत यह हेडर अवरुद्ध होना।  2

  6. Microsoft Learn, <condition_variable>. इस पर कि कंडीशन वेरिएबल पर प्रतीक्षा के लिए म्यूटेक्स चाहिए, प्रतीक्षा के दौरान लॉक छूटा रहता है; स्प्यूरियस वेकअप हैं — सूचना बिना जागना — इसलिए प्रतीक्षा पक्ष को लौटने पर शर्त स्पष्ट फिर जाँचनी चाहिए, और प्रेडिकेट रूप wait(lock, pred) वह लूप आपके लिए चलाता है; तथा condition_variable_any किसी भी म्यूटेक्स प्रकार से जुड़ सकता है।  2

  7. Microsoft Learn, <future>. इस पर कि future और shared_future के डिस्ट्रक्टर नियमतः ब्लॉक नहीं करते, एकमात्र अपवाद यह कि std::async से शुरू कार्य से बँधा future (या अंतिम shared_future) कार्य अधूरा रहते डिस्ट्रक्टर चलने पर साझा अवस्था ready होने तक ब्लॉक करता है — व्यवहार जो मानक में स्पष्ट नोट है।  2

  8. Microsoft Learn, About Synchronization. Win32 सिंक्रनाइज़ेशन प्रिमिटिव चुनने के मार्गदर्शन पर: पोर्टेबिलिटी-प्राथमिक C++ कोड के लिए std::mutex / std::shared_mutex और RAII सुझाए गए; Win32 प्रतीक्षा API या क्रॉस-प्रोसेस सिंक्रनाइज़ेशन चाहिए तब Win32 सिंक्रनाइज़ेशन ऑब्जेक्ट; नए इंट्रा-प्रोसेस कोड का डिफ़ॉल्ट SRW लॉक, CRITICAL_SECTION पुनरावर्ती लेने के लिए सुरक्षित; और इंट्रा-प्रोसेस सिंक्रनाइज़ेशन के लिए Mutex उपयोग «आम गलती» है क्योंकि हमेशा कर्नेल संक्रमण होता है।  2 3

  9. cppreference.com, std::thread::~thread. इस पर कि std::thread का डिस्ट्रक्टर std::terminate बुलाता है यदि थ्रेड अभी joinable रहते (न join न detach) बुलाया जाए — अर्थात थ्रेड ऑब्जेक्ट नष्ट होने से पहले join या detach का निर्णय बिना अपवाद पूरा होना चाहिए। 

  10. Microsoft Learn, Best Practices in the Parallel Patterns Library. इस पर कि समांतरता आदर्श रूप से यथासंभव ऊँचे स्तर (बाहरी लूप) पर व्यक्त हो; छोटी या असंतुलित प्रति-पुनरावृत्ति वाले समांतर लूप में fork/join शेड्यूलिंग ओवरहेड समांतर निष्पादन के लाभ से बढ़ सकता है; और प्रोसेसर संख्या बढ़ने पर वह प्रवृत्ति मज़बूत होती है। 

  11. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. इस पर कि C++17 समांतर एल्गोरिद्म लाइब्रेरी पूर्ण है, जबकि «पूर्ण» का अर्थ हर एल्गोरिद्म हर मामले में समांतर होना नहीं; सबसे महत्वपूर्ण एल्गोरिद्म समांतर करने और जो नहीं हैं उनके लिए भी निष्पादन-नीति हस्ताक्षर देने की कार्यान्वयन नीति पर। 

  12. Microsoft Learn, C++ standard library header files. इस पर कि मल्टीथ्रेडिंग-संबंधित मानक हेडर <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) के रूप में व्यवस्थित हैं। 

  13. Microsoft Learn, Dynamic-Link Library Best Practices. इस पर कि DllMain लोडर लॉक पकड़े बुलाया जाता है, जो API बुला सकता है उस पर गंभीर बाधाएँ लगाता है; DllMain के भीतर दूसरे थ्रेड से सिंक्रनाइज़ करना डेडलॉक कर सकता है; LoadLibrary बुलाना या थ्रेड खत्म होने की प्रतीक्षा विशिष्ट निषिद्ध क्रियाएँ हैं; आरंभीकरण यथासंभव विलंबित कर DllMain से बाहर ले जाना चाहिए; और लोडर लॉक को शीर्ष पर रखकर लॉक पदानुक्रम परिभाषित करना चाहिए। 

  14. Microsoft Learn, <thread>. इस पर कि <thread> हेडर thread वर्ग और sleep_for जैसी सहायक फ़ंक्शन परिभाषित करता है; /clr से कंपाइल कोड में यह हेडर अवरुद्ध है; और STDCPP_THREADS मैक्रो से थ्रेड समर्थन है या नहीं जाना जा सकता है। 

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

व्यावहारिक मल्टीथ्रेडिंग सर्वोत्तम अभ्यास: C संस्करण — Win32 API तरीके से सुरक्षित लिखना

Win32 के साथ C में मल्टीथ्रेडिंग का स्थापित तरीका _beginthreadex से थ्रेड बनाना, SRW लॉक और कंडीशन वेरिएबल, Interlocked फ़ंक्शन, और स्टॉप...

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

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

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

कंडीशन वेरिएबल की प्रतीक्षा सूचना आए बिना भी लौट सकती है (स्प्यूरियस वेकअप)। यह लेख Windows इम्प्लीमेंटेशन से समझाता है कि विनिर्देश इसे ...

व्यावहारिक मल्टीथ्रेडिंग बेस्ट प्रैक्टिस: Java संस्करण — वर्चुअल थ्रेड युग की रीतियाँ

Java में मल्टीथ्रेडिंग की स्थापित रीति यह है कि थ्रेड सीधे न बनाएँ, बल्कि ExecutorService और वर्चुअल थ्रेड पर चलें। यह लेख व्यावहारिक सिद...

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

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

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

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

std::mutex और Win32 के CRITICAL_SECTION / SRW लॉक में कैसे चुनें?
पोर्टेबिलिटी को प्राथमिकता देने वाले सामान्य C++ कोड के लिए std::mutex / std::shared_mutex साथ RAII रैपर (lock_guard / scoped_lock) पहला विकल्प हैं। Win32 सिंक्रनाइज़ेशन ऑब्जेक्ट तब लें जब उन्हें WaitForMultipleObjects जैसी Win32 प्रतीक्षा API से जोड़ना हो, या जब नामित ऑब्जेक्ट से क्रॉस-प्रोसेस सिंक्रनाइज़ेशन चाहिए। प्रोसेस के भीतर Win32 API सीधे उपयोग कर रहे हों तो नए कोड का डिफ़ॉल्ट SRW लॉक है, और CRITICAL_SECTION केवल तब जब उसी थ्रेड को उसे पुनरावर्ती रूप से लेना हो। इंट्रा-प्रोसेस बहिष्करण के लिए Win32 Mutex उपयोग करना क्लासिक गलती है, क्योंकि उसमें हमेशा कर्नेल संक्रमण होता है और वह उसी अनुपात में धीमा है।
std::thread का detach() उपयोग करना ठीक है?
नियम के रूप में, बचें। detached थ्रेड join का हर साधन खो देता है, और प्रोसेस निकलते समय वह अभी चल रहा है या नहीं, उस पर नियंत्रण जाता रहता है। क्लासिक दुर्घटना: detached थ्रेड स्थैतिक चर या हीप नष्ट होने के बाद भी चलता रहता है, और शटडाउन क्रैश करता है। थ्रेड के खत्म होने की प्रतीक्षा कर पाना थ्रेड डिज़ाइन की मूल आवश्यकता है, इसलिए jthread (जो स्वतः join करता है) उपयोग करें, या thread हो तो कोड को स्कोप खत्म होने से पहले हमेशा join करने की संरचना दें। detach केवल उस संकीर्ण स्थिति में स्वीकार्य है जहाँ थ्रेड प्रोसेस की नियति साझा कर सकता है और आप गारंटी दे सकें कि वह साझा स्थिति को छूता ही नहीं।
C++ में थ्रेडों के बीच सिंक्रनाइज़ेशन के लिए volatile उपयोग हो सकता है?
नहीं। C++ का volatile उन पढ़ने-लिखने के लिए क्वालिफायर है जिन्हें आप कंपाइलर से ऑप्टिमाइज़ कर मिटवाना नहीं चाहते — उदाहरण के लिए मेमोरी-मैप्ड I/O — और वह थ्रेडों के बीच दृश्यता या क्रम की गारंटी नहीं देता। कई थ्रेड बिना सिंक्रनाइज़ेशन के एक ही चर पर पहुँचें तो वह डेटा रेस है, और वह अपरिभाषित व्यवहार है। थ्रेडों के बीच साझा फ़्लैग और काउंटर के लिए std::atomic उपयोग करें, और कई चर एक साथ सुरक्षित करने हों तो std::mutex। std::atomic ऑपरेशन की अविभाज्यता और memory_order पर आधारित क्रम दोनों देता है।
std::async सुविधाजनक लगता है, पर क्या जाल हैं?
सबसे बड़ा जाल future का डिस्ट्रक्टर है। std::async से शुरू कार्य से बँधा future (या अंतिम shared_future) कार्य अधूरा रहते डिस्ट्रक्टर चलने पर पूरा होने तक ब्लॉक करता है। लौटे future को पकड़े बिना फेंक दें तो वह वहीं तुल्यकालिक निष्पादन के बराबर हो जाता है — दुर्घटना जिसमें अतुल्यकालिक जाना चाहा और क्रमबद्ध रह गए। साथ ही, लॉन्च नीति न बताएँ तो काम वास्तव में अलग थ्रेड पर चलता है या नहीं, कार्यान्वयन के विवेक पर है। उपयोग करें तो future का जीवन स्पष्ट रूप से सँभालें, और जहाँ समवर्ती निष्पादन गारंटी चाहिए वहाँ std::launch::async निर्दिष्ट करें।

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

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

Go Komura

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

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

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

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