एक मानक ने C++ को मल्टीथ्रेडिंग की अराजकता से कैसे बचाया
\n\nकल्पना करें: आप C++ में एक मल्टीथ्रेडेड प्रोग्राम लिख रहे हैं, और यह आपके लैपटॉप पर पूरी तरह से काम करता है। आप कोड को सर्वर पर अपलोड करते हैं - और यह क्रैश हो जाता है। या, इससे भी बदतर, यह गलत परिणाम देता है, लेकिन महीने में केवल एक बार। परिचित लगता है? 2011 तक यह सामान्य था। C++ के पास कोई एकीकृत मेमोरी मॉडल नहीं था - नियमों का एक सेट जो यह बताता है कि विभिन्न थ्रेड डेटा में बदलावों को कैसे देखते हैं। प्रत्येक कंपाइलर और प्रोसेसर अपनी मर्जी से काम करते थे। परिणाम - अराजकता, डेटा रेस और मल्टीथ्रेडिंग के प्रति कोसना।
\n\nC++11 ने सब कुछ बदल दिया। इसने एक मानकीकृत मेमोरी मॉडल पेश किया। सूखा लगता है? लेकिन ऐसा नहीं है। यह मॉडल सुरक्षित और कुशल मल्टीथ्रेडिंग के लिए एक वास्तविक आधार है। इस लेख में हम हुड के नीचे देखेंगे: मेमोरी मॉडल कैसे काम करता है, परमाणु संचालन और मेमोरी बैरियर क्या हैं, और यह आपके कोड को कैसे प्रभावित करता है। यदि आप C++ में लिखते हैं और चाहते हैं कि आपके प्रोग्राम पूर्वानुमानित हों, तो आगे पढ़ें।
\n\nसमस्या: मेमोरी मॉडल के बिना दुनिया
\n\nC++11 से पहले, C++ में मल्टीथ्रेडिंग वाइल्ड वेस्ट की तरह थी। आपके पास थ्रेड थे (pthreads, Windows API या Boost के माध्यम से), लेकिन भाषा मानक ने यह नहीं बताया कि वे मेमोरी के माध्यम से कैसे इंटरैक्ट करते हैं। कंपाइलर ऑप्टिमाइज़ेशन के लिए निर्देशों को पुनर्व्यवस्थित कर सकता था, प्रोसेसर कैश में लेखन का क्रम बदल सकता था। सिंगल-थ्रेडेड कोड में यह सुरक्षित है: परिणाम नहीं बदलता है। लेकिन मल्टीथ्रेडेड में - यह एक आपदा है।
\n\nसबसे सरल उदाहरण पर विचार करें:
\n\n// तत्परता ध्वज और डेटा\nint data = 0;\nbool ready = false;\n\n// थ्रेड 1 (उत्पादक)\nvoid producer() {\n data = 42; // (1)\n ready = true; // (2)\n}\n\n// थ्रेड 2 (उपभोक्ता)\nvoid consumer() {\n while (!ready); // ध्वज की प्रतीक्षा करें\n std::cout << data; // 42 की उम्मीद है\n}\n\nसहज रूप से ऐसा लगता है: यदि ready == true, तो data पहले से ही 42 के बराबर है। लेकिन मेमोरी मॉडल के बिना, कंपाइलर (1) और (2) को आपस में बदल सकता है! या प्रोसेसर ready के लेखन को data के लेखन से पहले दूसरे कोर को दृश्यमान बना सकता है। परिणामस्वरूप, उपभोक्ता ready == true देखेगा, लेकिन data == 0 पढ़ेगा। एक बग जिसे डीबगर से पकड़ना असंभव है।
प्रोग्रामर मैन्युअल रूप से इससे लड़ते थे: मेमोरी बैरियर (asm volatile) डालते थे, वोलाटाइल वेरिएबल्स का उपयोग करते थे (जो समस्या का समाधान नहीं करते), प्रार्थना करते थे। C++11 ने अंततः स्पष्ट नियम दिए।
\n\nC++11 मेमोरी मॉडल क्या है?
\n\nमेमोरी मॉडल औपचारिक नियम हैं जो यह निर्धारित करते हैं कि किन परिस्थितियों में एक थ्रेड दूसरे थ्रेड द्वारा किए गए परिवर्तनों को देखने की गारंटी देता है। यह सवालों के जवाब देता है:
\n\n- \n
- एक वेरिएबल में लेखन दूसरे थ्रेड को कब दिखाई देता है? \n
- विभिन्न थ्रेड्स के संचालन किस क्रम में देखे जा सकते हैं? \n
- डेटा रेस (data race) क्या है और इससे कैसे बचें? \n
C++11 का मुख्य नवाचार परमाणु संचालन (std::atomic<T>) और मेमोरी ऑर्डरिंग के छह मॉडल (memory_order) हैं। वे प्रोग्रामर को यह नियंत्रण देते हैं कि कंपाइलर और प्रोसेसर संचालन को कैसे पुनर्व्यवस्थित करते हैं।
अब हम समझते हैं कि यह हुड के नीचे कैसे काम करता है।
\n\nपरमाणुता और क्रम: मॉडल के दो स्तंभ
\n\nपरमाणु संचालन
\n\nपरमाणु संचालन वह संचालन है जो या तो पूरी तरह से निष्पादित होता है या बिल्कुल नहीं होता है। अन्य थ्रेड्स के लिए यह अविभाज्य है। उदाहरण के लिए, x86 पर int को पढ़ना या लिखना आमतौर पर परमाणु होता है (यदि संरेखित है), लेकिन ++count नहीं है (यह read-modify-write है)। C++11 बुनियादी प्रकारों के लिए परमाणुता की गारंटी देने के लिए std::atomic<T> प्रदान करता है।
#include <atomic>\n\nstd::atomic<int> counter(0);\n\n// परमाणु वृद्धि\nvoid increment() {\n counter.fetch_add(1); // counter++ के बराबर, लेकिन परमाणु\n}\n\nलेकिन परमाणुता केवल आधा काम है। दूसरा आधा है
ब्लॉग
पुस्तक अनुशंसाएँ
यह भी पढ़ें