परिचय: C++ में समवर्तीता डरावनी क्यों नहीं, बल्कि दिलचस्प क्यों है?
\n\nएक समय था, जब सिंगल-कोर प्रोसेसर के युग में, प्रोग्राम सख्ती से क्रमिक रूप से निष्पादित होते थे। आज, आपका स्मार्टफोन भी 8 कोर वाला है, और सर्वरों में तो दर्जनों कोर होते हैं। इन सभी कोर को अपने लिए काम पर लगाना - यही असली प्रदर्शन का जादू है। और C++, सबसे शक्तिशाली और निम्न-स्तरीय भाषाओं में से एक, हमें इसके लिए उपकरणों का एक पूरा शस्त्रागार देता है। लेकिन भ्रमित कैसे न हों? थ्रेड्स, कोरूटीन (जिन्हें अक्सर Go की तरह 'गोरूटीन' कहा जाता है), async/await - क्या चुनें?
\n\nइस लेख में, हम आधुनिक C++ समवर्तीता को विस्तार से समझेंगे। आप जानेंगे कि कब क्लासिक थ्रेड्स का उपयोग करना चाहिए, और कब C++20 के हल्के कोरूटीन का, और कैसे async/await अतुल्यकालिक प्रोग्रामिंग को सरल बनाता है। चलिए शुरू करते हैं!
\n\nक्लासिक: std::thread के साथ मल्टीथ्रेडिंग
\n\nसबसे स्पष्ट से शुरू करते हैं - थ्रेड्स। std::thread ऑपरेटिंग सिस्टम के सिस्टम थ्रेड्स तक सीधी पहुँच प्रदान करता है। प्रत्येक थ्रेड निष्पादन का एक अलग संदर्भ है, जो एक अलग CPU कोर पर काम कर सकता है।
सरल उदाहरण: समानांतर गणना
\n\nकल्पना करें कि आपको 1 से 10 मिलियन तक की संख्याओं के वर्गों का योग निकालना है। क्रमिक रूप से करने में समय लगेगा। आइए इसे समानांतर करें:
\n\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n result = 0;\n for (int i = start; i <= end; ++i) {\n result += i * i;\n }\n}\n\nint main() {\n const int N = 10'000'000;\n const int NUM_THREADS = 4;\n std::vector<std::thread> threads;\n std::vector<long long> results(NUM_THREADS, 0);\n \n int chunk = N / NUM_THREADS;\n for (int i = 0; i < NUM_THREADS; ++i) {\n int start = i * chunk + 1;\n int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n }\n \n for (auto& t : threads) {\n t.join();\n }\n \n long long total = std::accumulate(results.begin(), results.end(), 0LL);\n std::cout << "वर्गों का योग: " << total << std::endl;\n return 0;\n}\n\n\nयहाँ हम 4 थ्रेड बनाते हैं, प्रत्येक अपनी सीमा के हिस्से की गणना करता है। परिणामों को एक वेक्टर में एकत्र किया जाता है। सब कुछ सरल और पूर्वानुमानित है। लेकिन कुछ बारीकियाँ हैं:
\n\n- \n
- ओवरहेड: प्रत्येक थ्रेड बनाना महंगा है (स्टैक आवंटन, सिस्टम कॉल)। हजारों छोटे कार्यों के लिए यह अक्षम है। \n
- सिंक्रोनाइज़ेशन: यदि थ्रेड साझा डेटा तक पहुँचते हैं, तो म्यूटेक्स की आवश्यकता होती है, जो ब्लॉकिंग का कारण बन सकते हैं और प्रदर्शन को कम कर सकते हैं। \n
- सीमित संख्या: थ्रेड्स की संख्या आमतौर पर CPU कोर की संख्या (हाइपरथ्रेडिंग को ध्यान में रखते हुए) से अधिक नहीं होनी चाहिए। \n
भारी, लंबी गणनाओं के लिए थ्रेड्स एक उत्कृष्ट विकल्प हैं। लेकिन I/O संचालन (फ़ाइल पढ़ना, नेटवर्क अनुरोध) के लिए वे अक्सर अत्यधिक होते हैं। यहाँ कोरूटीन दृश्य में आते हैं।
\n\nC++20 की क्रांति: कोरूटीन (गोरूटीन)
\n\nC++20 में कोरूटीन केवल एक और सुविधा नहीं है। यह एक मौलिक परिवर्तन है कि हम अतुल्यकालिक कोड कैसे लिखते हैं। थ्रेड्स के विपरीत, कोरूटीन हल्के सह-प्रोग्राम हैं जो अपने निष्पादन को रोक सकते हैं और बाद में फिर से शुरू कर सकते हैं, बिना OS थ्रेड को ब्लॉक किए।
\n\nकई डेवलपर उन्हें 'गोरूटीन' कहते हैं क्योंकि वे Go में goroutines के समान हैं, हालाँकि तकनीकी रूप से ये अलग अवधारणाएँ हैं। मुख्य लाभ: आप एक ही थ्रेड पर प्रदर्शन खोए बिना सैकड़ों हजारों कोरूटीन चला सकते हैं।
\n\nयह कैसे काम करता है?
\n\nकोरूटीन एक फ़ंक्शन है जिसमें कीवर्ड co_await, co_return या co_yield होता है। कंपाइलर स्वचालित रूप से इसे एक स्टेट मशीन में बदल देता है। यहाँ एक बुनियादी उदाहरण है:
#include <iostream>\n#include <coroutine>\n#incl