std::thread, std::jthread और थ्रेड मैनेजमेंट
std::thread और C++20 के std::jthread से थ्रेड बनाना और उन्हें मैनेज करना सीखें। join बनाम detach, स्टॉप टोकन के साथ सहयोगी कैंसलेशन (cooperative cancellation), थ्रेड-लोकल स्टोरेज और थ्रेड की सीमाओं के पार एक्सेप्शन हैंडलिंग को समझें।
कंकरेंसी क्यों?
आधुनिक CPU में कई कोर होते हैं, और उनका लाभ उठाने के लिए आपके प्रोग्राम को काम एक साथ (concurrently) चलाना होगा। C++11 ने में एक पोर्टेबल थ्रेडिंग लाइब्रेरी पेश की, और C++20 ने std::jthread के साथ उसे बेहतर बनाया। थ्रेड (thread) निष्पादन का एक स्वतंत्र पथ है जो उसी एड्रेस स्पेस को साझा करता है जिसमें उसे बनाने वाला थ्रेड है — यही उसकी ताकत भी है और ख़तरा भी।
प्रोसेस के विपरीत, थ्रेड मेमोरी साझा करते हैं। इससे संचार तेज़ होता है, लेकिन अगर दो थ्रेड बिना सिंक्रोनाइज़ेशन के एक ही वेरिएबल को पढ़ें और लिखें, तो डेटा रेस (data race) भी पैदा होती है। सिंक्रोनाइज़ेशन (अगले लेसन में) पर जाने से पहले, आइए पहले सीखें कि थ्रेड कैसे बनाएँ, मैनेज करें और सही तरीके से बंद करें।
std::thread से थ्रेड बनाना
आप std::thread को एक कॉलेबल (फ़ंक्शन, लैम्ब्डा, फ़ंक्टर) और उसके आर्गुमेंट देकर बनाते हैं। थ्रेड के कंस्ट्रक्ट होते ही तुरंत चलना शुरू कर देता है।
#include <iostream>
#include <thread>
void greet(const std::string& name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "! (" << i + 1 << ")\n";
}
}
int main() {
// Launch a thread running greet("Alice", 3)
std::thread t1(greet, "Alice", 3);
// Lambda-based thread
std::thread t2([]() {
std::cout << "Running in a lambda thread\n";
});
// MUST join or detach before t1/t2 are destroyed
t1.join(); // block until t1 finishes
t2.join();
std::cout << "All threads finished\n";
return 0;
}join() बनाम detach()
जो भी std::thread किसी चल रहे थ्रेड को दर्शाता है, उसे उसके डिस्ट्रक्टर के चलने से पहले या तो जॉइन (joined) या डिटैच (detached) करना ज़रूरी है। अगर आप ऐसा करना भूल गए, तो std::terminate() कॉल होता है और आपका प्रोग्राम क्रैश हो जाता है।
join() कॉल करने वाले थ्रेड को तब तक ब्लॉक रखता है जब तक लक्ष्य थ्रेड समाप्त न हो जाए। यही सुरक्षित डिफ़ॉल्ट है — आपको पता होता है कि थ्रेड पूरा हो चुका है और उसके नतीजे तैयार हैं।
detach() std::thread ऑब्जेक्ट और OS थ्रेड के बीच का संबंध तोड़ देता है। थ्रेड बैकग्राउंड में चलता रहता है (एक "डेमन थ्रेड") और आप उसे दोबारा जॉइन नहीं कर सकते। सावधान रहें: अगर डिटैच किया गया थ्रेड उस फ़ंक्शन के स्टैक वेरिएबल एक्सेस करता है जिसने उसे बनाया था, और वह फ़ंक्शन लौट जाता है, तो आपको अपरिभाषित व्यवहार (undefined behavior) मिलता है।
ऐसे किसी थ्रेड को कभी detach() न करें जो बनाने वाले स्कोप के लोकल वेरिएबल को कैप्चर करता हो या उनका रेफ़रेंस रखता हो। जब बनाने वाला फ़ंक्शन लौटता है, तो वे लोकल नष्ट हो जाते हैं और डिटैच किया गया थ्रेड डैंग्लिंग मेमोरी को एक्सेस करता है। यह अपरिभाषित व्यवहार (undefined behavior) है और अक्सर बीच-बीच में होने वाले क्रैश के रूप में दिखता है। अगर आपको डिटैच करना ही पड़े, तो डेटा वैल्यू द्वारा (by value) पास करें या लाइफ़टाइम बढ़ाने के लिए std::shared_ptr का उपयोग करें।
std::jthread — RAII थ्रेड (C++20)
std::jthread std::thread की दो समस्याओं को हल करता है: (1) यह अपने डिस्ट्रक्टर में अपने-आप जॉइन करता है, इसलिए आप इसे कभी भूल नहीं सकते, और (2) यह std::stop_token के ज़रिए सहयोगी कैंसलेशन (cooperative cancellation) का समर्थन करता है।
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token stoken, int id) {
while (!stoken.stop_requested()) {
std::cout << "Worker " << id << " running...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Worker " << id << " stopping gracefully\n";
}
int main() {
std::jthread jt1(worker, 1); // stop_token is auto-passed as first arg
std::jthread jt2(worker, 2);
std::this_thread::sleep_for(std::chrono::seconds(1));
// Request both threads to stop
jt1.request_stop();
jt2.request_stop();
// No need to call join() — jthread's destructor does it
std::cout << "Main thread exiting\n";
return 0;
}हार्डवेयर की जानकारी और थ्रेड-लोकल स्टोरेज
std::thread::hardware_concurrency() उन समवर्ती थ्रेड की संख्या लौटाता है जिन्हें हार्डवेयर सपोर्ट करता है। thread_local स्टोरेज ड्यूरेशन हर थ्रेड को वेरिएबल की अपनी अलग कॉपी देता है।
#include <iostream>
#include <thread>
#include <vector>
thread_local int tls_counter = 0; // each thread gets its own copy
void count_work(int id) {
for (int i = 0; i < 1000; ++i) {
++tls_counter; // no data race — each thread has its own
}
std::cout << "Thread " << id << ": tls_counter = " << tls_counter << "\n";
}
int main() {
unsigned int n = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << n << " threads\n";
std::vector<std::thread> threads;
for (unsigned int i = 0; i < n; ++i) {
threads.emplace_back(count_work, i);
}
for (auto& t : threads) {
t.join();
}
// Each thread printed 1000 — they don't share tls_counter
return 0;
}C++20 और उसके बाद के संस्करणों में std::thread की बजाय std::jthread को प्राथमिकता दें। यह अपने डिस्ट्रक्टर में अपने-आप जॉइन करता है, जिससे जॉइन करना भूलने पर std::terminate कॉल होने का जोखिम ख़त्म हो जाता है। इसका बिल्ट-इन std::stop_token तंत्र सहयोगी कैंसलेशन का अनुरोध करने का एक साफ़, मानकीकृत तरीक़ा देता है — अपना atomic फ़्लैग बनाने की ज़रूरत नहीं।
थ्रेड के पार एक्सेप्शन हैंडलिंग
किसी थ्रेड के अंदर फेंका गया ऐसा एक्सेप्शन, जिसे उसी थ्रेड के भीतर नहीं पकड़ा गया, std::terminate() को कॉल करता है। एक्सेप्शन जॉइन करने वाले थ्रेड तक अपने-आप नहीं पहुँचते।
थ्रेड के बीच एक्सेप्शन भेजने के लिए, वर्कर थ्रेड के अंदर उन्हें std::current_exception() से कैप्चर करें और std::exception_ptr में सँभालकर रखें। फिर जॉइन करने वाला थ्रेड std::rethrow_exception() से उन्हें दोबारा फेंक सकता है। std::future तंत्र (लेसन 4 में बताया गया है) यह काम अपने-आप सँभाल लेता है।
std::threadतुरंत चलना शुरू कर देता है; इसके डिस्ट्रक्टर के चलने से पहले आपकोjoin()याdetach()करना ज़रूरी हैstd::jthread(C++20) अपने डिस्ट्रक्टर में अपने-आप जॉइन करता है औरstd::stop_tokenके ज़रिए सहयोगी कैंसलेशन का समर्थन करता हैthread_localहर थ्रेड को वेरिएबल की अपनी स्वतंत्र कॉपी देता है — सिंक्रोनाइज़ेशन की ज़रूरत नहींstd::thread::hardware_concurrency()बताता है कि हार्डवेयर कितने थ्रेड एक साथ चला सकता है- थ्रेड में हुए एक्सेप्शन कॉलर तक नहीं पहुँचते — उन्हें भेजने के लिए
std::exception_ptrयाstd::futureका उपयोग करें - डिटैच किए गए थ्रेड को स्पॉन करने वाले स्कोप के लोकल वेरिएबल का रेफ़रेंस कभी न रखने दें
क्विज़ — अपना ज्ञान परखें
(15 XP)1. अगर किसी जॉइन करने योग्य थ्रेड को दर्शाने वाला `std::thread` ऑब्जेक्ट `join()` या `detach()` कॉल किए बिना नष्ट हो जाए, तो क्या होता है?
2. `std::jthread` सहयोगी कैंसलेशन का समर्थन कैसे करता है?
3. `thread_local` कीवर्ड क्या करता है?