Android के लिए एसएमपी प्राइमर

Android 3.0 और इसके बाद के वर्शन वाले प्लैटफ़ॉर्म को मल्टीप्रोसेसर आर्किटेक्चर के साथ काम करने के लिए ऑप्टिमाइज़ किया गया है. इस दस्तावेज़ में उन समस्याओं के बारे में बताया गया है जो C, C++, और Java प्रोग्रामिंग लैंग्वेज (इसे कम शब्दों में "Java" कहा जाता है) में सिमेट्रिक मल्टीप्रोसेसर सिस्टम के लिए मल्टीथ्रेडेड कोड लिखते समय आ सकती हैं. यह Android ऐप्लिकेशन डेवलपर के लिए एक गाइड है. इसमें इस विषय पर पूरी जानकारी नहीं दी गई है.

परिचय

एसएमपी, “सिमेट्रिक मल्टी-प्रोसेसर” का छोटा नाम है. यह एक ऐसा डिज़ाइन है जिसमें दो या उससे ज़्यादा एक जैसे सीपीयू कोर, मुख्य मेमोरी का ऐक्सेस शेयर करते हैं. कुछ साल पहले तक, सभी Android डिवाइसों में यूपी (यूनी-प्रोसेसर) होता था.

ज़्यादातर Android डिवाइसों में हमेशा से ही कई सीपीयू होते हैं. हालांकि, पहले इनमें से सिर्फ़ एक सीपीयू का इस्तेमाल ऐप्लिकेशन चलाने के लिए किया जाता था. वहीं, बाकी सीपीयू डिवाइस के अलग-अलग हार्डवेयर (जैसे, रेडियो) को मैनेज करते थे. सीपीयू का आर्किटेक्चर अलग-अलग हो सकता है. साथ ही, उन पर चल रहे प्रोग्राम, एक-दूसरे से कम्यूनिकेट करने के लिए मुख्य मेमोरी का इस्तेमाल नहीं कर सकते थे.

आज-कल बेचे जाने वाले ज़्यादातर Android डिवाइस, एसएमपी डिज़ाइन के हिसाब से बनाए जाते हैं. इससे सॉफ़्टवेयर डेवलपर के लिए चीज़ें थोड़ी ज़्यादा मुश्किल हो जाती हैं. मल्टी-थ्रेडेड प्रोग्राम में रेस कंडिशन की वजह से, यूनिप्रोसेसर पर कोई समस्या नहीं दिखती. हालांकि, जब आपके दो या इससे ज़्यादा थ्रेड, अलग-अलग कोर पर एक साथ चल रहे हों, तो यह समस्या बार-बार आ सकती है. इसके अलावा, अलग-अलग प्रोसेसर आर्किटेक्चर या एक ही आर्किटेक्चर के अलग-अलग इंप्लिमेंटेशन पर कोड चलाने से, उसके काम न करने की संभावना कम या ज़्यादा हो सकती है. ऐसा कोड जिसे x86 पर अच्छी तरह से टेस्ट किया गया है, वह ARM पर काम करना बंद कर सकता है. अगर कोड को किसी नए कंपाइलर के साथ फिर से कंपाइल किया जाता है, तो हो सकता है कि वह काम न करे.

इस दस्तावेज़ के बाकी हिस्से में इसकी वजह बताई जाएगी. साथ ही, आपको यह भी बताया जाएगा कि आपको क्या करना है, ताकि यह पक्का किया जा सके कि आपका कोड सही तरीके से काम करे.

मेमोरी कंसिस्टेंसी मॉडल: एसएमपी थोड़े अलग क्यों होते हैं

यह एक जटिल विषय की खास जानकारी है, जिसे तेज़ी से और आकर्षक तरीके से पेश किया गया है. कुछ सेक्शन में जानकारी अधूरी हो सकती है, लेकिन उसमें कोई भी जानकारी गलत या गुमराह करने वाली नहीं होनी चाहिए. जैसा कि आपको अगले सेक्शन में दिखेगा, यहां दी गई जानकारी आम तौर पर ज़रूरी नहीं होती.

इस विषय के बारे में ज़्यादा जानकारी पाने के लिए, दस्तावेज़ के आखिर में मौजूद ज़्यादा जानकारी सेक्शन देखें.

मेमोरी कंसिस्टेंसी मॉडल या अक्सर सिर्फ़ “मेमोरी मॉडल”, मेमोरी ऐक्सेस के बारे में प्रोग्रामिंग लैंग्वेज या हार्डवेयर आर्किटेक्चर की गारंटी के बारे में बताते हैं. उदाहरण के लिए, अगर आपने पता A के लिए कोई वैल्यू लिखी है और फिर पता B के लिए कोई वैल्यू लिखी है, तो मॉडल यह गारंटी दे सकता है कि हर सीपीयू कोर को वे वैल्यू उसी क्रम में दिखें.

ज़्यादातर प्रोग्रामर, सीक्वेंशियल कंसिस्टेंसी मॉडल का इस्तेमाल करते हैं. इसके बारे में (Adve और Gharachorloo) ने इस तरह बताया है:

  • ऐसा लगता है कि मेमोरी से जुड़े सभी ऑपरेशन एक-एक करके पूरे होते हैं
  • एक ही थ्रेड में होने वाले सभी ऑपरेशन, उस प्रोसेसर के प्रोग्राम में बताए गए क्रम में ही होते हैं.

मान लें कि हमारे पास एक बहुत ही आसान कंपाइलर या इंटरप्रेटर है, जो कोई गड़बड़ी नहीं करता: यह सोर्स कोड में असाइनमेंट को लोड और स्टोर करने के निर्देशों में बिलकुल उसी क्रम में अनुवाद करता है, जिसमें हर ऐक्सेस के लिए एक निर्देश होता है. हम यह भी मानेंगे कि हर थ्रेड अपने प्रोसेसर पर चलता है.

अगर किसी कोड को देखने पर पता चलता है कि वह मेमोरी से कुछ डेटा पढ़ता और लिखता है, तो क्रम के हिसाब से काम करने वाले सीपीयू आर्किटेक्चर पर, आपको पता होता है कि कोड उन डेटा को उम्मीद के मुताबिक क्रम में पढ़ेगा और लिखेगा. ऐसा हो सकता है कि सीपीयू, निर्देशों को फिर से क्रम में लगा रहा हो और डेटा को पढ़ने और लिखने में देरी कर रहा हो. हालांकि, डिवाइस पर चल रहे कोड के लिए यह बताना मुमकिन नहीं है कि सीपीयू, निर्देशों को सीधे तौर पर लागू करने के अलावा कुछ और कर रहा है. हम मेमोरी-मैप किए गए डिवाइस ड्राइवर के इनपुट/आउटपुट को अनदेखा कर देंगे.

इन बातों को समझाने के लिए, कोड के छोटे स्निपेट का इस्तेमाल करना फ़ायदेमंद होता है. इन्हें आम तौर पर लिटमस टेस्ट कहा जाता है.

यहां एक आसान उदाहरण दिया गया है, जिसमें कोड दो थ्रेड पर चल रहा है:

थ्रेड 1 थ्रेड 2
A = 3
B = 5
reg0 = B
reg1 = A

इस उदाहरण और आने वाले सभी लिटमस उदाहरणों में, मेमोरी लोकेशन को अंग्रेज़ी के बड़े अक्षरों (A, B, C) से दिखाया गया है. साथ ही, सीपीयू रजिस्टर “reg” से शुरू होते हैं. शुरुआत में, सभी मेमोरी की वैल्यू शून्य होती है. निर्देशों को ऊपर से नीचे की ओर लागू किया जाता है. यहां थ्रेड 1, लोकेशन A पर वैल्यू 3 और लोकेशन B पर वैल्यू 5 को सेव करता है. थ्रेड 2, जगह B से वैल्यू को reg0 में लोड करता है. इसके बाद, जगह A से वैल्यू को reg1 में लोड करता है. (ध्यान दें कि हम एक क्रम में लिख रहे हैं और दूसरे क्रम में पढ़ रहे हैं.)

थ्रेड 1 और थ्रेड 2 को अलग-अलग सीपीयू कोर पर एक्ज़ीक्यूट किया जाता है. मल्टी-थ्रेडेड कोड के बारे में सोचते समय, आपको हमेशा यह अनुमान लगाना चाहिए.

क्रम के मुताबिक एक जैसा होने की गारंटी यह है कि दोनों थ्रेड के एक्ज़ीक्यूशन के बाद, रजिस्टर इनमें से किसी एक स्थिति में होंगे:

रजिस्टर करें राज्य
reg0=5, reg1=3 संभव है (थ्रेड 1 पहले चला)
reg0=0, reg1=0 संभव है (थ्रेड 2 पहले चला)
reg0=0, reg1=3 संभव है (एक साथ कई कार्रवाइयां करना)
reg0=5, reg1=0 कभी नहीं

ऐसी स्थिति में आने के लिए जहां हम A को स्टोर करने से पहले B=5 देखते हैं, या तो रीड या राइट को क्रम से बाहर होना होगा. क्रम के मुताबिक काम करने वाली मशीन पर ऐसा नहीं हो सकता.

x86 और ARM जैसे यूनि-प्रोसेसर, आम तौर पर क्रम के हिसाब से काम करते हैं. थ्रेड, इंटरलीव्ड तरीके से काम करते हैं, क्योंकि ओएस कर्नेल उनके बीच स्विच करता है. ज़्यादातर एसएमपी सिस्टम, जिनमें x86 और ARM शामिल हैं, क्रम के हिसाब से एक जैसे नहीं होते. उदाहरण के लिए, हार्डवेयर के लिए मेमोरी में जाने से पहले स्टोर को बफ़र करना आम बात है, ताकि वे तुरंत मेमोरी में न पहुंचें और अन्य कोर को न दिखें.

इनकी जानकारी काफ़ी अलग होती है. उदाहरण के लिए, x86, क्रम के हिसाब से एक जैसा नहीं होता. हालांकि, यह अब भी गारंटी देता है कि reg0 = 5 और reg1 = 0 होना नामुमकिन है. स्टोर बफ़र किए जाते हैं, लेकिन उनका क्रम बरकरार रहता है. वहीं, ARM में ऐसा नहीं होता. बफ़र किए गए स्टोर का क्रम बनाए नहीं रखा जाता. साथ ही, हो सकता है कि स्टोर एक ही समय पर अन्य सभी कोर तक न पहुंचें. ये अंतर, असेंबली प्रोग्रामर के लिए ज़रूरी हैं. हालांकि, जैसा कि हम नीचे देखेंगे, C, C++ या Java प्रोग्रामर इस तरह से प्रोग्राम कर सकते हैं और उन्हें करना चाहिए कि आर्किटेक्चर के ऐसे अंतर छिप जाएं.

अब तक, हमने यह मान लिया था कि सिर्फ़ हार्डवेयर ही निर्देशों को फिर से क्रम में लगाता है. असल में, कंपाइलर भी परफ़ॉर्मेंस को बेहतर बनाने के लिए, निर्देशों को फिर से क्रम में लगाता है. हमारे उदाहरण में, कंपाइलर यह तय कर सकता है कि थ्रेड 2 में बाद के कुछ कोड को reg0 से पहले reg1 की वैल्यू की ज़रूरत है. इसलिए, वह पहले reg1 को लोड करेगा. इसके अलावा, हो सकता है कि पहले से मौजूद किसी कोड ने A को पहले ही लोड कर लिया हो और कंपाइलर, A को फिर से लोड करने के बजाय उस वैल्यू का फिर से इस्तेमाल करने का फ़ैसला ले. दोनों ही मामलों में, reg0 और reg1 के लोड को फिर से क्रम में लगाया जा सकता है.

हार्डवेयर या कंपाइलर में, अलग-अलग मेमोरी लोकेशन के ऐक्सेस को फिर से क्रम में लगाने की अनुमति है. ऐसा इसलिए, क्योंकि इससे किसी एक थ्रेड के एक्ज़ीक्यूशन पर असर नहीं पड़ता और इससे परफ़ॉर्मेंस को काफ़ी बेहतर बनाया जा सकता है. जैसा कि हम देखेंगे, थोड़ी सावधानी बरतने पर, हम इसे मल्टीथ्रेडेड प्रोग्राम के नतीजों पर असर डालने से भी रोक सकते हैं.

कंपाइलर, मेमोरी ऐक्सेस को फिर से क्रम में लगा सकते हैं. इसलिए, यह समस्या एसएमपी के लिए नई नहीं है. यूनिप्रोसेसर पर भी, कंपाइलर हमारे उदाहरण में reg0 और reg1 के लोड को फिर से क्रम में लगा सकता है. साथ ही, थ्रेड 1 को फिर से क्रम में लगाए गए निर्देशों के बीच शेड्यूल किया जा सकता है. लेकिन अगर हमारा कंपाइलर फिर से क्रम में नहीं लगाता, तो हमें शायद कभी भी यह समस्या नहीं दिखती. ज़्यादातर एआरएम एसएमपी पर, कंपाइलर के फिर से क्रम में लगाए बिना भी, फिर से क्रम में लगाने की प्रोसेस दिख सकती है. ऐसा हो सकता है कि कई बार प्रोसेस को सही तरीके से पूरा करने के बाद ऐसा हो. अगर असेंबली लैंग्वेज में प्रोग्रामिंग नहीं की जा रही है, तो एसएमपी से आम तौर पर उन समस्याओं के दिखने की संभावना बढ़ जाती है जो पहले से मौजूद थीं.

डेटा-रेस-फ़्री प्रोग्रामिंग

अच्छी बात यह है कि आम तौर पर, इन सभी चीज़ों के बारे में सोचे बिना भी आसानी से काम किया जा सकता है. अगर कुछ आसान नियमों का पालन किया जाए, तो आम तौर पर "क्रम के मुताबिक एक जैसा होना" वाले हिस्से को छोड़कर, पिछले सेक्शन की सभी बातों को भूल जाना सुरक्षित होता है. अगर आपने गलती से उन नियमों का उल्लंघन किया, तो आपको अन्य समस्याओं का सामना करना पड़ सकता है.

मॉडर्न प्रोग्रामिंग लैंग्वेज, "डेटा-रेस-फ़्री" प्रोग्रामिंग स्टाइल को बढ़ावा देती हैं. जब तक आप "डेटा रेस" को शामिल न करने का वादा करते हैं और कुछ ऐसे कंस्ट्रक्ट से बचते हैं जो कंपाइलर को कुछ और बताते हैं, तब तक कंपाइलर और हार्डवेयर, लगातार एक जैसे नतीजे देने का वादा करते हैं. इसका मतलब यह नहीं है कि वे मेमोरी ऐक्सेस के क्रम को बदलने से बचते हैं. इसका मतलब है कि अगर नियमों का पालन किया जाए, तो यह नहीं बताया जा सकता कि मेमोरी ऐक्सेस को फिर से क्रम में लगाया जा रहा है. यह कुछ ऐसा है कि आपको बताया जाए कि सॉसेज एक स्वादिष्ट और भूख बढ़ाने वाला खाना है, लेकिन आपको सॉसेज फ़ैक्ट्री में जाने की अनुमति नहीं है. डेटा रेस से, मेमोरी को फिर से क्रम में लगाने की समस्या का पता चलता है.

"डेटा रेस" क्या है?

डेटा रेस तब होती है, जब कम से कम दो थ्रेड एक ही सामान्य डेटा को एक साथ ऐक्सेस करते हैं और उनमें से कम से कम एक थ्रेड उसमें बदलाव करता है. "सामान्य डेटा" से हमारा मतलब ऐसी चीज़ से है जो खास तौर पर थ्रेड कम्यूनिकेशन के लिए सिंक किया गया ऑब्जेक्ट नहीं है. म्यूटेक्स, कंडिशन वैरिएबल, Java वोलेटाइल या C++ ऐटॉमिक ऑब्जेक्ट, सामान्य डेटा नहीं होते. साथ ही, इनके ऐक्सेस को रेस करने की अनुमति होती है. असल में, इनका इस्तेमाल अन्य ऑब्जेक्ट पर डेटा रेस को रोकने के लिए किया जाता है.

यह तय करने के लिए कि दो थ्रेड एक ही समय पर एक ही मेमोरी लोकेशन को ऐक्सेस करते हैं या नहीं, हम ऊपर दी गई मेमोरी-रीऑर्डरिंग की चर्चा को अनदेखा कर सकते हैं. साथ ही, क्रम के मुताबिक एक जैसा होने का अनुमान लगा सकते हैं. अगर A और B सामान्य बूलियन वैरिएबल हैं, जो शुरुआत में गलत हैं, तो इस प्रोग्राम में डेटा रेस नहीं है:

थ्रेड 1 थ्रेड 2
if (A) B = true if (B) A = true

ऑपरेशन का क्रम नहीं बदला जाता है. इसलिए, दोनों शर्तें गलत होंगी और कोई भी वैरिएबल अपडेट नहीं होगा. इसलिए, डेटा रेस नहीं हो सकती. इस बारे में सोचने की ज़रूरत नहीं है कि अगर थ्रेड 1 में A से लोड करने और B में स्टोर करने का क्रम किसी तरह बदल जाए, तो क्या होगा. कंपाइलर को थ्रेड 1 को "B = true; if (!A) B = false" के तौर पर फिर से लिखकर, उसका क्रम बदलने की अनुमति नहीं है. ऐसा करना, दिन के उजाले में शहर के बीचों-बीच सॉसेज बनाने जैसा होगा.

डेटा रेस को आधिकारिक तौर पर, पहले से मौजूद बुनियादी टाइप के आधार पर तय किया जाता है. जैसे, इंटीजर और रेफ़रंस या पॉइंटर. किसी int को असाइन करने के साथ-साथ, उसे किसी दूसरे थ्रेड में पढ़ना, साफ़ तौर पर डेटा रेस है. हालांकि, C++ स्टैंडर्ड लाइब्रेरी और Java कलेक्शन लाइब्रेरी, दोनों को इस तरह से लिखा गया है कि लाइब्रेरी लेवल पर भी डेटा रेस के बारे में पता लगाया जा सके. वे वादा करते हैं कि डेटा रेस तब तक नहीं होगी, जब तक एक ही कंटेनर को एक साथ ऐक्सेस न किया जाए. साथ ही, कम से कम एक ऐक्सेस ऐसा होना चाहिए जो कंटेनर को अपडेट करे. एक थ्रेड में set<T> को अपडेट करने के साथ-साथ, दूसरे थ्रेड में उसे पढ़ने से, लाइब्रेरी में डेटा रेस की समस्या आ सकती है. इसे अनौपचारिक तौर पर, "लाइब्रेरी-लेवल डेटा रेस" कहा जा सकता है. इसके उलट, एक थ्रेड में एक set<T> को अपडेट करने और दूसरे थ्रेड में किसी दूसरे को पढ़ने से, डेटा रेस नहीं होती. इसकी वजह यह है कि लाइब्रेरी, उस मामले में (लो-लेवल) डेटा रेस को शामिल न करने का वादा करती है.

आम तौर पर, डेटा स्ट्रक्चर में अलग-अलग फ़ील्ड को एक साथ ऐक्सेस करने से, डेटा रेस की समस्या नहीं होती. हालांकि, इस नियम का एक अहम अपवाद है: C या C++ में बिट-फ़ील्ड के लगातार सीक्वेंस को एक "मेमोरी लोकेशन" माना जाता है. इस तरह के क्रम में किसी भी बिट-फ़ील्ड को ऐक्सेस करने का मतलब है कि डेटा रेस की मौजूदगी का पता लगाने के लिए, उन सभी को ऐक्सेस किया जा रहा है. इससे पता चलता है कि आम हार्डवेयर, आस-पास की बिट को पढ़े और फिर से लिखे बिना, अलग-अलग बिट को अपडेट नहीं कर सकते. Java प्रोग्रामर को इस तरह की कोई समस्या नहीं होती.

डेटा रेस से बचना

डेटा रेस से बचने के लिए, मॉडर्न प्रोग्रामिंग लैंग्वेज में सिंक करने के कई तरीके उपलब्ध होते हैं. सबसे बुनियादी टूल ये हैं:

लॉक या म्यूटेक्स
Mutexes (C++11 std::mutex या pthread_mutex_t) या Java में synchronized ब्लॉक का इस्तेमाल करके, यह पक्का किया जा सकता है कि कोड का कोई सेक्शन, एक ही डेटा को ऐक्सेस करने वाले कोड के अन्य सेक्शन के साथ न चले. हम इन और मिलती-जुलती अन्य सुविधाओं को आम तौर पर "लॉक" कहेंगे. शेयर किए गए डेटा स्ट्रक्चर को ऐक्सेस करने से पहले, लगातार एक खास लॉक हासिल करना और बाद में उसे रिलीज़ करना, डेटा स्ट्रक्चर को ऐक्सेस करते समय डेटा रेस को रोकता है. यह यह भी पक्का करता है कि अपडेट और ऐक्सेस ऐटॉमिक हों, यानी कि डेटा स्ट्रक्चर में कोई अन्य अपडेट बीच में नहीं चल सकता. यह डेटा रेस को रोकने के लिए, अब तक का सबसे आम टूल है. Java synchronized ब्लॉक या C++ lock_guard या unique_lock का इस्तेमाल करके, यह पक्का करें कि अपवाद की स्थिति में लॉक सही तरीके से रिलीज़ हो जाएं.
वोलाटाइल/ऐटॉमिक वैरिएबल
Java में volatile फ़ील्ड होते हैं. ये फ़ील्ड, डेटा रेस की समस्या पैदा किए बिना, एक साथ ऐक्सेस करने की सुविधा देते हैं साल 2011 से, C और C++ में atomic वैरिएबल और फ़ील्ड के लिए, एक जैसे सिमैंटिक का इस्तेमाल किया जा सकता है. आम तौर पर, इन्हें इस्तेमाल करना लॉक की तुलना में ज़्यादा मुश्किल होता है. ऐसा इसलिए, क्योंकि ये सिर्फ़ यह पक्का करते हैं कि किसी एक वैरिएबल के लिए अलग-अलग ऐक्सेस, ऐटॉमिक हों. C++ में, आम तौर पर यह इंक्रीमेंट जैसे आसान रीड-मॉडिफ़ाई-राइट ऑपरेशन तक सीमित होता है. Java में इसके लिए, खास तरीके से कॉल करने की ज़रूरत होती है.) लॉक के उलट, volatile या atomic वैरिएबल का इस्तेमाल सीधे तौर पर नहीं किया जा सकता. ऐसा इसलिए, ताकि अन्य थ्रेड, लंबे कोड सीक्वेंस में रुकावट न डाल सकें.

यह ध्यान रखना ज़रूरी है कि C++ और Java में volatile के अलग-अलग मतलब होते हैं. C++ में, volatile डेटा रेस को नहीं रोकता. हालांकि, पुराना कोड अक्सर atomic ऑब्जेक्ट की कमी को पूरा करने के लिए, इसका इस्तेमाल करता है. अब इसका सुझाव नहीं दिया जाता. C++ में, उन वैरिएबल के लिए atomic<T> का इस्तेमाल करें जिन्हें एक साथ कई थ्रेड ऐक्सेस कर सकते हैं. C++ volatile का इस्तेमाल, डिवाइस रजिस्टर वगैरह के लिए किया जाता है.

C/C++ atomic वैरिएबल या Java volatile वैरिएबल का इस्तेमाल करके, अन्य वैरिएबल पर डेटा रेस को रोका जा सकता है. अगर flag को atomic<bool> या atomic_bool(C/C++) या volatile boolean (Java) टाइप का बताया गया है और शुरुआत में यह गलत है, तो नीचे दिया गया स्निपेट डेटा-रेस-फ़्री है:

थ्रेड 1 थ्रेड 2
A = ...
  flag = true
while (!flag) {}
... = A

थ्रेड 2, flag के सेट होने का इंतज़ार करता है. इसलिए, थ्रेड 2 में A का ऐक्सेस, थ्रेड 1 में A के असाइनमेंट के बाद होना चाहिए, न कि उसके साथ. इसलिए, A पर डेटा रेस नहीं होती. flag पर होने वाली रेस को डेटा रेस नहीं माना जाता, क्योंकि वोलेटाइल/ऐटॉमिक ऐक्सेस, "सामान्य मेमोरी ऐक्सेस" नहीं होते.

मेमोरी को फिर से क्रम में लगाने से रोकने या छिपाने के लिए, इसे लागू करना ज़रूरी है. इससे, लिटमस टेस्ट जैसे कोड उम्मीद के मुताबिक काम कर पाएंगे. आम तौर पर, इससे वोलेटाइल/ऐटॉमिक मेमोरी ऐक्सेस, सामान्य ऐक्सेस की तुलना में काफ़ी ज़्यादा महंगे हो जाते हैं.

हालांकि, ऊपर दिया गया उदाहरण डेटा-रेस-फ़्री है, लेकिन Java में Object.wait() या C/C++ में कंडिशन वैरिएबल के साथ लॉक, आम तौर पर बेहतर समाधान देते हैं. इनमें बैटरी पावर खत्म होने के दौरान, लूप में इंतज़ार नहीं करना पड़ता.

यादों के क्रम में बदलाव कब दिखता है

डेटा-रेस-फ़्री प्रोग्रामिंग, आम तौर पर हमें मेमोरी ऐक्सेस को फिर से क्रम में लगाने से जुड़ी समस्याओं से बचाती है. हालांकि, ऐसे कई मामले हैं जिनमें क्रम में किया गया बदलाव दिखता है:
  1. अगर आपके प्रोग्राम में कोई गड़बड़ी है, जिसकी वजह से अनजाने में डेटा रेस हो जाती है, तो कंपाइलर और हार्डवेयर में हुए बदलाव दिख सकते हैं. साथ ही, आपके प्रोग्राम का व्यवहार हैरान करने वाला हो सकता है. उदाहरण के लिए, अगर हम पिछले उदाहरण में flag volatile को डिक्लेयर करना भूल जाते हैं, तो Thread 2 को A दिख सकता है, जिसे शुरू नहीं किया गया है. इसके अलावा, कंपाइलर यह तय कर सकता है कि थ्रेड 2 के लूप के दौरान फ़्लैग में बदलाव नहीं किया जा सकता. साथ ही, प्रोग्राम को इसमें बदल सकता है
    थ्रेड 1 थ्रेड 2
    A = ...
      flag = true
    reg0 = flag; while (!reg0) {}
    ... = A
    डीबग करते समय, आपको लूप हमेशा के लिए जारी रहता हुआ दिख सकता है. भले ही, flag सही हो.
  2. C++ में, क्रम से चलने वाली प्रोसेस को साफ़ तौर पर आसान बनाने की सुविधाएं मिलती हैं. भले ही, कोई रेस न हो. ऐटमिक ऑपरेशन में, साफ़ तौर पर memory_order_... आर्ग्युमेंट इस्तेमाल किए जा सकते हैं. इसी तरह, java.util.concurrent.atomic पैकेज में मिलती-जुलती सुविधाओं का ज़्यादा सीमित सेट मिलता है. खास तौर पर, lazySet(). साथ ही, Java प्रोग्रामर कभी-कभी इसी तरह के असर के लिए, जान-बूझकर डेटा रेस का इस्तेमाल करते हैं. इन सभी से परफ़ॉर्मेंस में सुधार होता है. हालांकि, प्रोग्रामिंग की जटिलता की वजह से, इसमें ज़्यादा खर्च आता है. हमने नीचे उनके बारे में संक्षेप में बताया है.
  3. C और C++ का कुछ कोड पुराने स्टाइल में लिखा गया है. यह मौजूदा लैंग्वेज स्टैंडर्ड के मुताबिक नहीं है. इसमें atomic वैरिएबल के बजाय volatile वैरिएबल का इस्तेमाल किया जाता है. साथ ही, फ़ेंस या बैरियर डालकर, मेमोरी ऑर्डर करने की साफ़ तौर पर अनुमति नहीं दी जाती है. इसके लिए, ऐक्सेस को फिर से क्रम में लगाने और हार्डवेयर मेमोरी मॉडल को समझने के बारे में साफ़ तौर पर तर्क देना ज़रूरी है. इस तरह की कोडिंग स्टाइल का इस्तेमाल, अब भी Linux कर्नेल में किया जाता है. इसका इस्तेमाल नए Android ऐप्लिकेशन में नहीं किया जाना चाहिए. इसके बारे में यहां ज़्यादा जानकारी भी नहीं दी गई है.

प्रैक्टिस करें

मेमोरी कंसिस्टेंसी से जुड़ी समस्याओं को डीबग करना बहुत मुश्किल हो सकता है. अगर लॉक, atomic या volatile डिक्लेरेशन मौजूद न होने की वजह से, कुछ कोड पुराना डेटा पढ़ते हैं, तो हो सकता है कि आपको डीबगर की मदद से मेमोरी डंप की जांच करने पर भी इसकी वजह पता न चले. जब तक डीबगर क्वेरी जारी की जाती है, तब तक हो सकता है कि सीपीयू कोर ने ऐक्सेस के पूरे सेट को देख लिया हो. साथ ही, मेमोरी और सीपीयू रजिस्टर का कॉन्टेंट “असंभव” स्थिति में दिखे.

C में क्या नहीं करना चाहिए

यहां हमने गलत कोड के कुछ उदाहरण दिए हैं. साथ ही, उन्हें ठीक करने के आसान तरीके भी बताए हैं. ऐसा करने से पहले, हमें भाषा की एक बुनियादी सुविधा के इस्तेमाल के बारे में बात करनी होगी.

"volatile" और C/C++

C और C++ volatile डिक्लेरेशन, खास मकसद के लिए इस्तेमाल किए जाने वाले टूल हैं. ये कंपाइलर को वोलेटाइल ऐक्सेस को फिर से क्रम में लगाने या हटाने से रोकते हैं. यह सुविधा, हार्डवेयर डिवाइस के रजिस्टर को ऐक्सेस करने वाले कोड, एक से ज़्यादा जगहों पर मैप की गई मेमोरी या setjmp से जुड़े कोड के लिए मददगार हो सकती है. लेकिन C और C++ volatile, Java volatile की तरह, थ्रेड कम्यूनिकेशन के लिए डिज़ाइन नहीं किए गए हैं.

C और C++ में, volatile डेटा के ऐक्सेस को नॉन-वॉलेटाइल डेटा के ऐक्सेस के साथ फिर से क्रम में लगाया जा सकता है. साथ ही, ऐटमिसिटी की कोई गारंटी नहीं होती. इसलिए, पोर्टेबल कोड में थ्रेड के बीच डेटा शेयर करने के लिए, volatile का इस्तेमाल नहीं किया जा सकता. भले ही, वह यूनिप्रोसेसर पर ही क्यों न हो. आम तौर पर, C volatile हार्डवेयर को ऐक्सेस का क्रम बदलने से नहीं रोकता. इसलिए, मल्टी-थ्रेडेड एसएमपी एनवायरमेंट में यह कम काम का होता है. इसी वजह से, C11 और C++11 में atomic ऑब्जेक्ट काम करते हैं. आपको उनकी जगह पर, इन फ़ॉर्मैट का इस्तेमाल करना चाहिए.

volatile का इस्तेमाल, थ्रेड के बीच कम्यूनिकेशन के लिए अब भी किया जाता है. हालांकि, यह C और C++ कोड का पुराना तरीका है. यह अक्सर उस डेटा के लिए सही तरीके से काम करता है जो मशीन रजिस्टर में फ़िट होता है. हालांकि, इसके लिए ज़रूरी है कि इसका इस्तेमाल या तो साफ़ तौर पर फ़ेंस के साथ किया जाए या ऐसे मामलों में किया जाए जहां मेमोरी ऑर्डर करना ज़रूरी न हो. हालांकि, इस बात की गारंटी नहीं है कि यह आने वाले समय में कंपाइलर के साथ सही तरीके से काम करेगा.

उदाहरण

ज़्यादातर मामलों में, ऐटॉमिक ऑपरेशन के बजाय लॉक (जैसे, pthread_mutex_t या C++11 std::mutex) का इस्तेमाल करना बेहतर होता है. हालांकि, हम ऐटॉमिक ऑपरेशन का इस्तेमाल करके दिखाएंगे कि उन्हें असल स्थिति में कैसे इस्तेमाल किया जाएगा.

MyThing* gGlobalThing = NULL;  // Wrong!  See below.
void initGlobalThing()    // runs in Thread 1
{
    MyStruct* thing = malloc(sizeof(*thing));
    memset(thing, 0, sizeof(*thing));
    thing->x = 5;
    thing->y = 10;
    /* initialization complete, publish */
    gGlobalThing = thing;
}
void useGlobalThing()    // runs in Thread 2
{
    if (gGlobalThing != NULL) {
        int i = gGlobalThing->x;    // could be 5, 0, or uninitialized data
        ...
    }
}

यहां आइडिया यह है कि हम एक स्ट्रक्चर तय करते हैं, उसके फ़ील्ड को शुरू करते हैं, और आखिर में हम उसे ग्लोबल वैरिएबल में सेव करके “पब्लिश” करते हैं. इस पॉइंट पर, कोई भी दूसरा थ्रेड इसे देख सकता है. हालांकि, इससे कोई फ़र्क़ नहीं पड़ता, क्योंकि यह पूरी तरह से शुरू हो चुका है, है न?

समस्या यह है कि फ़ील्ड शुरू होने से पहले, gGlobalThing में स्टोर करने की प्रोसेस देखी जा सकती है. आम तौर पर, ऐसा इसलिए होता है, क्योंकि कंपाइलर या प्रोसेसर ने gGlobalThing और thing->x में स्टोर करने की प्रोसेस को फिर से क्रम में लगाया है. thing->x से डेटा पढ़ने वाला कोई दूसरा थ्रेड, 5, 0 या ऐसा डेटा देख सकता है जिसे शुरू नहीं किया गया है.

यहां मुख्य समस्या, gGlobalThing पर डेटा रेस की है. अगर थ्रेड 1, initGlobalThing() को कॉल करता है और थ्रेड 2, useGlobalThing() को कॉल करता है, तो gGlobalThing को लिखे जाने के दौरान पढ़ा जा सकता है.

gGlobalThing को ऐटॉमिक के तौर पर डिक्लेयर करके, इस समस्या को ठीक किया जा सकता है. C++11 में:

atomic<MyThing*> gGlobalThing(NULL);

इससे यह पक्का होता है कि राइट, अन्य थ्रेड को सही क्रम में दिखेंगे. यह कुछ अन्य गड़बड़ियों को रोकने की गारंटी भी देता है. हालांकि, ये गड़बड़ियां असल Android हार्डवेयर पर होने की संभावना नहीं होती. उदाहरण के लिए, यह पक्का करता है कि हम ऐसा gGlobalThing पॉइंटर न देख पाएं जो सिर्फ़ कुछ हद तक लिखा गया हो.

जावा में क्या नहीं करना चाहिए

हमने Java भाषा की कुछ काम की सुविधाओं के बारे में बात नहीं की है. इसलिए, पहले हम उन पर एक नज़र डालेंगे.

तकनीकी तौर पर, Java में कोड को डेटा-रेस-फ़्री होना ज़रूरी नहीं है. साथ ही, बहुत सावधानी से लिखा गया Java कोड का एक छोटा हिस्सा होता है, जो डेटा रेस की मौजूदगी में सही तरीके से काम करता है. हालांकि, ऐसा कोड लिखना बेहद मुश्किल है. इसलिए, हम यहां इसके बारे में सिर्फ़ संक्षेप में बात करेंगे. इस समस्या को और गंभीर बनाने के लिए, जिन विशेषज्ञों ने इस कोड का मतलब बताया था वे अब इस बात से सहमत नहीं हैं कि यह जानकारी सही है. (यह स्पेसिफ़िकेशन, डेटा-रेस-फ़्री कोड के लिए सही है.)

फ़िलहाल, हम डेटा-रेस-फ़्री मॉडल का इस्तेमाल करेंगे. इसके लिए, Java भी C और C++ जैसी ही गारंटी देता है. इसके अलावा, यह भाषा कुछ ऐसे प्रिमिटिव भी उपलब्ध कराती है जो साफ़ तौर पर क्रम में चलने वाली एक जैसी प्रोसेस को आसान बनाते हैं. खास तौर पर, java.util.concurrent.atomic में lazySet() और weakCompareAndSet() कॉल. C और C++ की तरह, हम फ़िलहाल इन्हें अनदेखा करेंगे.

Java के "synchronized" और "volatile" कीवर्ड

“synchronized” कीवर्ड, Java प्रोग्रामिंग लैंग्वेज में पहले से मौजूद लॉकिंग मैकेनिज़्म उपलब्ध कराता है. हर ऑब्जेक्ट के साथ एक “मॉनिटर” जुड़ा होता है. इसका इस्तेमाल, एक-दूसरे से अलग ऐक्सेस देने के लिए किया जा सकता है. अगर दो थ्रेड एक ही ऑब्जेक्ट पर "सिंक" करने की कोशिश करते हैं, तो उनमें से एक थ्रेड तब तक इंतज़ार करेगा, जब तक दूसरा थ्रेड पूरा नहीं हो जाता.

जैसा कि हमने ऊपर बताया है, Java का volatile T, C++11 के atomic<T> के जैसा है. volatile फ़ील्ड को एक साथ ऐक्सेस किया जा सकता है. इससे डेटा रेस की समस्या नहीं होती. lazySet() वगैरह और डेटा रेस को अनदेखा करते हुए, Java VM का काम यह पक्का करना है कि नतीजा अब भी क्रम के हिसाब से एक जैसा दिखे.

खास तौर पर, अगर थ्रेड 1 किसी volatile फ़ील्ड में लिखता है और थ्रेड 2 बाद में उसी फ़ील्ड से पढ़ता है और नई वैल्यू देखता है, तो थ्रेड 2 को थ्रेड 1 की ओर से पहले किए गए सभी राइट भी दिखेंगे. मेमोरी इफ़ेक्ट के मामले में, वोलेटाइल में लिखना, मॉनिटर रिलीज़ करने जैसा है. साथ ही, वोलेटाइल से पढ़ना, मॉनिटर हासिल करने जैसा है.

C++ के atomic से एक अहम अंतर है: अगर हम Java में volatile int x; लिखते हैं, तो x++ और x = x + 1 एक ही हैं; यह एक ऐटॉमिक लोड करता है, नतीजे को बढ़ाता है, और फिर एक ऐटॉमिक स्टोर करता है. C++ के उलट, पूरी बढ़ोतरी ऐटॉमिक नहीं होती. इसके बजाय, ऐटॉमिक इंक्रीमेंट ऑपरेशन, java.util.concurrent.atomic से उपलब्ध कराए जाते हैं.

उदाहरण

यहां मोनोटोनिक काउंटर को लागू करने का एक आसान और गलत तरीका बताया गया है: (Java theory and practice: Managing volatility).

class Counter {
    private int mValue;
    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

मान लें कि get() और incr() को कई थ्रेड से कॉल किया जाता है. साथ ही, हम यह पक्का करना चाहते हैं कि get() को कॉल किए जाने पर, हर थ्रेड को मौजूदा संख्या दिखे. सबसे बड़ी समस्या यह है कि mValue++ में असल में तीन ऑपरेशन हैं:

  1. reg = mValue
  2. reg = reg + 1
  3. mValue = reg

अगर दो थ्रेड incr() में एक साथ लागू होते हैं, तो इनमें से एक अपडेट खो सकता है. इंक्रीमेंट को ऐटॉमिक बनाने के लिए, हमें incr() को “सिंक्रनाइज़” के तौर पर एलान करना होगा.

हालांकि, यह अब भी काम नहीं कर रहा है. खास तौर पर, एसएमपी पर. हालांकि, अब भी डेटा रेस की समस्या है. इसकी वजह यह है कि get(), mValue को incr() के साथ एक ही समय पर ऐक्सेस कर सकता है. Java के नियमों के तहत, get() कॉल को अन्य कोड के हिसाब से फिर से क्रम में लगाया जा सकता है. उदाहरण के लिए, अगर हम एक के बाद एक दो काउंटर पढ़ते हैं, तो हो सकता है कि नतीजे अलग-अलग दिखें. ऐसा इसलिए, क्योंकि हमने हार्डवेयर या कंपाइलर की मदद से, get() कॉल का क्रम बदला है. हम get() को सिंक्रनाइज़ करके, इस समस्या को ठीक कर सकते हैं. इस बदलाव के बाद, कोड सही हो जाता है.

हालांकि, इससे लॉक कंटेंशन की समस्या हो सकती है. इससे परफ़ॉर्मेंस पर असर पड़ सकता है. get() को सिंक्रनाइज़ करने के बजाय, हम mValue को “volatile” के साथ डिक्लेयर कर सकते हैं. (ध्यान दें कि incr() को अब भी synchronize का इस्तेमाल करना होगा, क्योंकि ऐसा न करने पर mValue++ एक सिंगल ऐटॉमिक ऑपरेशन नहीं होगा.) इससे डेटा रेस की समस्या भी नहीं होती. इसलिए, क्रम के मुताबिक डेटा की एक जैसी क्वालिटी बनी रहती है. incr() की रफ़्तार थोड़ी कम होगी, क्योंकि इसमें मॉनिटर एंट्री/एग्ज़िट ओवरहेड और वोलेटाइल स्टोर से जुड़ा ओवरहेड, दोनों शामिल होते हैं. हालांकि, get() की रफ़्तार ज़्यादा होगी. इसलिए, अगर कॉन्टेंट न होने पर भी रीड की संख्या, राइट की संख्या से ज़्यादा है, तो यह एक फ़ायदा है. (सिंक किए गए ब्लॉक को पूरी तरह से हटाने का तरीका जानने के लिए, AtomicInteger भी देखें.)

यहां एक और उदाहरण दिया गया है, जो C के पिछले उदाहरणों से मिलता-जुलता है:

class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;
    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }
    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

इसमें भी C कोड जैसी ही समस्या है. इसमें sGoodies पर डेटा रेस की समस्या है. इसलिए, goods में फ़ील्ड को शुरू करने से पहले, असाइनमेंट sGoodies = goods को देखा जा सकता है. अगर sGoodies को volatile कीवर्ड के साथ डिक्लेयर किया जाता है, तो क्रम से चलने वाली एक जैसी स्थिति को पहले जैसा कर दिया जाता है. साथ ही, चीज़ें उम्मीद के मुताबिक काम करेंगी.

ध्यान दें कि सिर्फ़ sGoodies रेफ़रंस ही वोलेटाइल होता है. इसके अंदर मौजूद फ़ील्ड के ऐक्सेस को नहीं. sGoodies के volatile होने और मेमोरी ऑर्डर को सही तरीके से सुरक्षित रखने के बाद, फ़ील्ड को एक साथ ऐक्सेस नहीं किया जा सकता. स्टेटमेंट z = sGoodies.x, MyClass.sGoodies का वोलेटाइल लोड करेगा. इसके बाद, sGoodies.x का नॉन-वोलेटाइल लोड करेगा. अगर आपने लोकल रेफ़रंस MyGoodies localGoods = sGoodies बनाया है, तो इसके बाद वाला z = localGoods.x कोई भी वोलेटाइल लोड नहीं करेगा.

Java प्रोग्रामिंग में एक आम मुहावरा है, “डबल-चेक्ड लॉकिंग”:

class MyClass {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

इसका मकसद यह है कि हम MyClass के इंस्टेंस से जुड़े Helper ऑब्जेक्ट का एक इंस्टेंस चाहते हैं. हमें इसे सिर्फ़ एक बार बनाना होगा. इसलिए, हम इसे एक खास getHelper() फ़ंक्शन के ज़रिए बनाते हैं और दिखाते हैं. दो थ्रेड एक साथ इंस्टेंस न बनाएं, इसके लिए हमें ऑब्जेक्ट बनाने की प्रोसेस को सिंक करना होगा. हालांकि, हम हर कॉल पर “सिंक्रनाइज़ किए गए” ब्लॉक के लिए ओवरहेड का पेमेंट नहीं करना चाहते. इसलिए, हम सिर्फ़ तब ऐसा करते हैं, जब helper की वैल्यू शून्य हो.

इसमें helper फ़ील्ड पर डेटा रेस है. इसे किसी दूसरे थ्रेड में helper == null के साथ सेट किया जा सकता है.

यह देखने के लिए कि यह कैसे काम नहीं कर सकता, उसी कोड को थोड़ा सा फिर से लिखें, जैसे कि इसे C जैसी भाषा में कंपाइल किया गया हो (मैंने Helper’s कंस्ट्रक्टर गतिविधि को दिखाने के लिए कुछ पूर्णांक फ़ील्ड जोड़े हैं):

if (helper == null) {
    synchronized() {
        if (helper == null) {
            newHelper = malloc(sizeof(Helper));
            newHelper->x = 5;
            newHelper->y = 10;
            helper = newHelper;
        }
    }
    return helper;
}

हार्डवेयर या कंपाइलर को helper के स्टोर को x/y फ़ील्ड के साथ फिर से क्रम में लगाने से रोकने के लिए कुछ भी नहीं है. किसी अन्य थ्रेड को helper नॉन-नल मिल सकता है, लेकिन इसके फ़ील्ड अभी तक सेट नहीं किए गए हैं और इस्तेमाल के लिए तैयार नहीं हैं. ज़्यादा जानकारी और गड़बड़ी के अन्य मोड के बारे में जानने के लिए, अपेंडिक्स में मौजूद “‘Double Checked Locking is Broken’ Declaration” लिंक पर जाएं. इसके अलावा, जोश ब्लोच की किताब Effective Java, 2nd Edition में आइटम 71 (“Use lazy initialization judiciously”) देखें.

इस समस्या को हल करने के दो तरीके हैं:

  1. सबसे आसान तरीका अपनाएं और बाहरी जांच को मिटाएं. इससे यह पक्का होता है कि हम सिंक्रनाइज़ किए गए ब्लॉक के बाहर, helper की वैल्यू की जांच कभी न करें.
  2. helper को वोलेटाइल के तौर पर डिक्लेयर करें. इस छोटे से बदलाव के बाद, उदाहरण J-3 में दिया गया कोड, Java 1.5 और इसके बाद के वर्शन पर सही तरीके से काम करेगा. (आपको यह बात सच मानने में एक मिनट लग सकता है.)

यहां volatile के व्यवहार का एक और उदाहरण दिया गया है:

class MyClass {
    int data1, data2;
    volatile int vol1, vol2;
    void setValues() {    // runs in Thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }
    void useValues() {    // runs in Thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
}

useValues() को देखते हुए, अगर थ्रेड 2 ने अब तक vol1 के अपडेट को नहीं देखा है, तो उसे यह नहीं पता चल सकता कि data1 या data2 को अब तक सेट किया गया है या नहीं. vol1 में अपडेट देखने के बाद, इसे पता चलता है कि data1 को सुरक्षित तरीके से ऐक्सेस किया जा सकता है और डेटा रेस के बिना सही तरीके से पढ़ा जा सकता है. हालांकि, यह data2 के बारे में कोई अनुमान नहीं लगा सकता, क्योंकि वह स्टोर, वोलेटाइल स्टोर के बाद किया गया था.

ध्यान दें कि volatile का इस्तेमाल, एक-दूसरे के साथ रेस करने वाले अन्य मेमोरी ऐक्सेस के क्रम को फिर से बदलने से रोकने के लिए नहीं किया जा सकता. इस बात की कोई गारंटी नहीं है कि यह मशीन मेमोरी फ़ेंस इंस्ट्रक्शन जनरेट करेगा. इसका इस्तेमाल, डेटा रेस को रोकने के लिए किया जा सकता है. इसके लिए, कोड को सिर्फ़ तब चलाया जाता है, जब कोई दूसरा थ्रेड किसी शर्त को पूरा करता हो.

क्या करें

C/C++ में, C++11 सिंक्रनाइज़ेशन क्लास को प्राथमिकता दें, जैसे कि std::mutex. अगर नहीं, तो इससे जुड़े pthread ऑपरेशन का इस्तेमाल करें. इनमें सही मेमोरी फ़ेंस शामिल हैं. इनसे Android प्लैटफ़ॉर्म के सभी वर्शन पर, सही (जब तक कि कुछ और न बताया गया हो, तब तक क्रम से एक जैसा) और बेहतर व्यवहार मिलता है. कृपया उनका सही तरीके से इस्तेमाल करें. उदाहरण के लिए, याद रखें कि कंडिशन वैरिएबल वेट, बिना सिग्नल के गलत तरीके से वापस आ सकते हैं. इसलिए, उन्हें लूप में दिखना चाहिए.

हमारा सुझाव है कि आप सीधे तौर पर ऐटॉमिक फ़ंक्शन का इस्तेमाल न करें. हालांकि, अगर लागू किया जा रहा डेटा स्ट्रक्चर बहुत आसान है, जैसे कि काउंटर, तो सीधे तौर पर ऐटॉमिक फ़ंक्शन का इस्तेमाल किया जा सकता है. pthread mutex को लॉक और अनलॉक करने के लिए, एक-एक ऐटॉमिक ऑपरेशन की ज़रूरत होती है. साथ ही, अगर कोई विवाद नहीं है, तो आम तौर पर इसकी लागत एक कैश मिस से कम होती है. इसलिए, mutex कॉल को ऐटॉमिक ऑपरेशन से बदलकर, ज़्यादा बचत नहीं की जा सकती. नॉन-ट्रिवियल डेटा स्ट्रक्चर के लिए लॉक-फ़्री डिज़ाइन को ज़्यादा ध्यान देने की ज़रूरत होती है. इससे यह पक्का किया जा सकता है कि डेटा स्ट्रक्चर पर हाई लेवल ऑपरेशन, ऐटॉमिक के तौर पर दिखें. ये ऑपरेशन, सिर्फ़ साफ़ तौर पर दिखने वाले ऐटॉमिक पीस के तौर पर नहीं, बल्कि पूरी तरह से ऐटॉमिक के तौर पर दिखने चाहिए.

memory_order... या lazySet() के साथ रिलैक्स ऑर्डरिंग का इस्तेमाल करने पर, परफ़ॉर्मेंस बेहतर हो सकती है. हालांकि, इसके लिए आपको ज़्यादा जानकारी की ज़रूरत होगी. इनका इस्तेमाल करने वाले मौजूदा कोड के एक बड़े हिस्से में, बाद में गड़बड़ियां पाई जाती हैं. अगर मुमकिन हो, तो इनसे बचें. अगर आपके इस्तेमाल के उदाहरण, अगले सेक्शन में दिए गए उदाहरणों से पूरी तरह मेल नहीं खाते हैं, तो पक्का करें कि आप विशेषज्ञ हों या आपने किसी विशेषज्ञ से सलाह ली हो.

C/C++ में थ्रेड कम्यूनिकेशन के लिए, volatile का इस्तेमाल करने से बचें.

Java में, कॉनकरंसी से जुड़ी समस्याओं को अक्सर java.util.concurrent पैकेज की सही यूटिलिटी क्लास का इस्तेमाल करके हल किया जाता है. कोड को अच्छी तरह से लिखा गया है और एसएमपी पर अच्छी तरह से टेस्ट किया गया है.

सबसे सुरक्षित तरीका यह है कि आप अपने ऑब्जेक्ट को इम्यूटेबल बनाएं. Java के String और Integer जैसी क्लास के ऑब्जेक्ट में ऐसा डेटा होता है जिसे ऑब्जेक्ट बनाने के बाद बदला नहीं जा सकता. इससे उन ऑब्जेक्ट पर डेटा रेस की संभावना नहीं रहती. Effective Java, 2nd Ed. किताब में, “Item 15: Minimize Mutability” में खास निर्देश दिए गए हैं. खास तौर पर, Java फ़ील्ड को “final" (Bloch) के तौर पर एलान करने की अहमियत पर ध्यान दें.

भले ही कोई ऑब्जेक्ट बदला न जा सकता हो, लेकिन याद रखें कि बिना किसी सिंक्रनाइज़ेशन के उसे किसी दूसरे थ्रेड पर भेजना, डेटा रेस है. कभी-कभी Java में ऐसा किया जा सकता है (नीचे देखें), लेकिन इसके लिए बहुत सावधानी बरतनी पड़ती है. साथ ही, इससे कोड के टूटने की संभावना होती है. अगर यह परफ़ॉर्मेंस के लिए बहुत ज़रूरी नहीं है, तो volatile एलान जोड़ें. C++ में, किसी डेटा रेस की तरह, सही सिंक के बिना किसी इम्यूटेबल ऑब्जेक्ट के पॉइंटर या रेफ़रंस को कम्यूनिकेट करना एक गड़बड़ी है. इस मामले में, ऐसा हो सकता है कि ऐप्लिकेशन रुक-रुककर क्रैश हो. उदाहरण के लिए, स्टोर के फिर से क्रम में लगाने की वजह से, रिसीव करने वाले थ्रेड को ऐसा मेथड टेबल पॉइंटर दिख सकता है जिसे शुरू नहीं किया गया है.

अगर न तो कोई मौजूदा लाइब्रेरी क्लास और न ही कोई इम्यूटेबल क्लास सही है, तो Java synchronized स्टेटमेंट या C++ lock_guard / unique_lock का इस्तेमाल किया जाना चाहिए. इससे, किसी ऐसे फ़ील्ड के ऐक्सेस को सुरक्षित किया जा सकता है जिसे एक से ज़्यादा थ्रेड से ऐक्सेस किया जा सकता है. अगर आपके मामले में म्यूटेक्स काम नहीं करते हैं, तो आपको शेयर किए गए फ़ील्ड volatile या atomic का एलान करना चाहिए. हालांकि, आपको थ्रेड के बीच होने वाले इंटरैक्शन को समझने के लिए बहुत सावधानी बरतनी होगी. इन एलान से आपको समवर्ती प्रोग्रामिंग की सामान्य गलतियों से बचने में मदद नहीं मिलेगी. हालांकि, इनसे आपको कंपाइलर को ऑप्टिमाइज़ करने और एसएमपी की गड़बड़ियों से जुड़ी रहस्यमयी गड़बड़ियों से बचने में मदद मिलेगी.

आपको किसी ऑब्जेक्ट के रेफ़रंस को "पब्लिश" करने से बचना चाहिए. इसका मतलब है कि उसे उसके कंस्ट्रक्टर में, अन्य थ्रेड के लिए उपलब्ध कराना. C++ में यह समस्या कम गंभीर होती है. इसके अलावा, अगर Java में "कोई डेटा रेस नहीं" वाली हमारी सलाह का पालन किया जाता है, तो भी यह समस्या कम गंभीर होती है. हालांकि, यह हमेशा एक अच्छी सलाह होती है. साथ ही, यह तब ज़रूरी हो जाती है, जब आपका Java कोड अन्य कॉन्टेक्स्ट में चलाया जाता है, जहां Java का सुरक्षा मॉडल मायने रखता है. साथ ही, ऐसा हो सकता है कि भरोसेमंद न होने वाला कोड, "लीक" हुए ऑब्जेक्ट रेफ़रंस को ऐक्सेस करके, डेटा रेस की समस्या पैदा कर दे. अगर हमारी चेतावनियों को अनदेखा करके, अगले सेक्शन में बताई गई कुछ तकनीकों का इस्तेमाल किया जाता है, तो यह भी गंभीर उल्लंघन माना जाएगा. ज़्यादा जानकारी के लिए, (Java में सुरक्षित कंस्ट्रक्शन तकनीक) देखें

कमज़ोर मेमोरी ऑर्डर के बारे में कुछ और जानकारी

C++11 और इसके बाद के वर्शन, डेटा-रेस-फ़्री प्रोग्राम के लिए, क्रम में चलने वाली कंसिस्टेंसी की गारंटी को कम करने के लिए साफ़ तौर पर तरीके उपलब्ध कराते हैं. ऐटॉमिक ऑपरेशन के लिए, साफ़ तौर पर दिए गए memory_order_relaxed, memory_order_acquire (सिर्फ़ लोड), और memory_order_release (सिर्फ़ स्टोर) आर्ग्युमेंट, डिफ़ॉल्ट तौर पर दिए गए की तुलना में कम गारंटी देते हैं. आम तौर पर , memory_order_seq_cst इंप्लिसिट होता है. memory_order_acq_rel ऐटॉमिक रीड-मॉडिफ़ाई राइट ऑपरेशन के लिए, memory_order_acquire और memory_order_release, दोनों की गारंटी देता है. memory_order_consume को अभी तक अच्छी तरह से तय या लागू नहीं किया गया है, ताकि यह काम का हो. इसलिए, इसे अभी के लिए अनदेखा किया जाना चाहिए.

Java.util.concurrent.atomic में मौजूद lazySet तरीके, C++ memory_order_release स्टोर की तरह ही हैं. Java के सामान्य वैरिएबल का इस्तेमाल, कभी-कभी memory_order_relaxed ऐक्सेस के विकल्प के तौर पर किया जाता है. हालांकि, ये असल में और भी कमज़ोर होते हैं. C++ के उलट, volatile के तौर पर एलान किए गए वैरिएबल को बिना क्रम के ऐक्सेस करने का कोई असल तरीका नहीं है.

आम तौर पर, आपको इनका इस्तेमाल करने से बचना चाहिए. हालांकि, अगर परफ़ॉर्मेंस से जुड़ी कोई समस्या है, तो इनका इस्तेमाल किया जा सकता है. ARM जैसे कमज़ोर क्रम वाले मशीन आर्किटेक्चर पर, इनका इस्तेमाल करने से आम तौर पर हर ऐटॉमिक ऑपरेशन के लिए कुछ दर्जन मशीन साइकल की बचत होगी. x86 पर, परफ़ॉर्मेंस में सुधार सिर्फ़ स्टोर तक सीमित है. साथ ही, इस पर ज़्यादा ध्यान नहीं दिया जाता. हालांकि, ज़्यादा कोर होने पर फ़ायदा कम हो सकता है, क्योंकि मेमोरी सिस्टम एक सीमित फ़ैक्टर बन जाता है.

कमज़ोर तरीके से क्रम में लगाए गए ऐटॉमिक के पूरे सिमैंटिक जटिल होते हैं. आम तौर पर, इसके लिए भाषा के नियमों की सटीक समझ होनी चाहिए. हम यहां इस बारे में बात नहीं करेंगे. उदाहरण के लिए:

  • कंपाइलर या हार्डवेयर, memory_order_relaxed ऐक्सेस को लॉक के अधिग्रहण और रिलीज़ से सीमित किए गए क्रिटिकल सेक्शन में ले जा सकता है. हालांकि, ऐक्सेस को क्रिटिकल सेक्शन से बाहर नहीं ले जाया जा सकता. इसका मतलब है कि दो memory_order_relaxed स्टोर, क्रम से बाहर दिख सकते हैं. भले ही, उन्हें किसी ज़रूरी सेक्शन से अलग किया गया हो.
  • जब किसी सामान्य Java वैरिएबल का इस्तेमाल, शेयर किए गए काउंटर के तौर पर गलत तरीके से किया जाता है, तो हो सकता है कि वह किसी दूसरे थ्रेड को कम होता हुआ दिखे. भले ही, उसे सिर्फ़ एक अन्य थ्रेड से बढ़ाया गया हो. हालांकि, C++ ऐटॉमिक के लिए ऐसा नहीं है memory_order_relaxed.

चेतावनी के तौर पर, हम यहां कुछ ऐसे मुहावरे दे रहे हैं जो कमज़ोर क्रम वाले ऐटॉमिक के इस्तेमाल के कई मामलों को कवर करते हैं. इनमें से कई सिर्फ़ C++ पर लागू होते हैं.

नॉन-रेसिंग ऐक्सेस

यह काफ़ी आम बात है कि कोई वैरिएबल ऐटॉमिक हो, क्योंकि कभी-कभी इसे लिखने के साथ-साथ पढ़ा जाता है. हालांकि, सभी ऐक्सेस में यह समस्या नहीं होती. उदाहरण के लिए, किसी वैरिएबल को ऐटॉमिक होना पड़ सकता है, क्योंकि उसे किसी ज़रूरी सेक्शन के बाहर पढ़ा जाता है. हालांकि, सभी अपडेट लॉक से सुरक्षित होते हैं. ऐसे में, एक ही लॉक से सुरक्षित की गई रीड प्रोसेस, रेस कंडीशन में नहीं आ सकती. इसकी वजह यह है कि एक साथ कई राइट प्रोसेस नहीं हो सकतीं. ऐसे मामले में, नॉन-रेसिंग ऐक्सेस (इस मामले में लोड) को memory_order_relaxed के साथ एनोटेट किया जा सकता है. इससे C++ कोड की शुद्धता में कोई बदलाव नहीं होता. लॉक लागू करने से, अन्य थ्रेड के ऐक्सेस के हिसाब से, मेमोरी के ज़रूरी क्रम को पहले ही लागू कर दिया जाता है. साथ ही, memory_order_relaxed से पता चलता है कि ऐटॉमिक ऐक्सेस के लिए, क्रम से जुड़ी कोई और पाबंदी लागू करने की ज़रूरत नहीं है.

Java में इसका कोई असल एनालॉग नहीं है.

नतीजे के सही होने पर भरोसा नहीं किया जा सकता

जब हम सिर्फ़ हिंट जनरेट करने के लिए रेसिंग लोड का इस्तेमाल करते हैं, तो आम तौर पर लोड के लिए मेमोरी ऑर्डर लागू न करना भी ठीक होता है. अगर वैल्यू भरोसेमंद नहीं है, तो हम अन्य वैरिएबल के बारे में कुछ भी अनुमान लगाने के लिए, नतीजे का इस्तेमाल भरोसेमंद तरीके से नहीं कर सकते. इसलिए, अगर मेमोरी ऑर्डर की गारंटी नहीं है, तो कोई बात नहीं. साथ ही, लोड को memory_order_relaxed आर्ग्युमेंट के साथ दिया जाता है.

इसका एक सामान्य उदाहरण, x को f(x) से बदलने के लिए, C++ compare_exchange का इस्तेमाल करना है. f(x) का हिसाब लगाने के लिए, x के शुरुआती लोड का भरोसेमंद होना ज़रूरी नहीं है. अगर हमसे कोई गलती होती है, तो compare_exchange नहीं हो पाएगा और हम फिर से कोशिश करेंगे. x के शुरुआती लोड के लिए, memory_order_relaxed आर्ग्युमेंट का इस्तेमाल किया जा सकता है. हालांकि, असल compare_exchange के लिए सिर्फ़ मेमोरी ऑर्डरिंग मायने रखती है.

अपने-आप बदला गया, लेकिन पढ़ा नहीं गया डेटा

कभी-कभी डेटा में एक साथ कई थ्रेड से बदलाव किया जाता है. हालांकि, इसकी जांच तब तक नहीं की जाती, जब तक कि एक साथ की जा रही गणना पूरी नहीं हो जाती. इसका एक अच्छा उदाहरण है, एक काउंटर जो कई थ्रेड से एक साथ अपने-आप बढ़ता है (जैसे, C++ में fetch_add() या C में atomic_fetch_add_explicit() का इस्तेमाल करके). हालांकि, इन कॉल के नतीजे को हमेशा अनदेखा किया जाता है. नतीजे के तौर पर मिलने वाली वैल्यू को सिर्फ़ आखिर में पढ़ा जाता है. ऐसा सभी अपडेट पूरे होने के बाद किया जाता है.

इस मामले में, यह बताने का कोई तरीका नहीं है कि इस डेटा के ऐक्सेस को फिर से क्रम में लगाया गया था या नहीं. इसलिए, C++ कोड memory_order_relaxed आर्ग्युमेंट का इस्तेमाल कर सकता है.

इवेंट के सामान्य काउंटर इसका एक उदाहरण हैं. चूंकि यह बहुत आम है, इसलिए इस मामले के बारे में कुछ अवलोकन करना उचित है:

  • memory_order_relaxed का इस्तेमाल करने से परफ़ॉर्मेंस बेहतर होती है, लेकिन हो सकता है कि इससे परफ़ॉर्मेंस से जुड़ी सबसे अहम समस्या हल न हो: हर अपडेट के लिए, काउंटर को होल्ड करने वाली कैश लाइन का खास ऐक्सेस ज़रूरी होता है. इस वजह से, जब भी कोई नया थ्रेड काउंटर को ऐक्सेस करता है, तो कैश मेमोरी में डेटा नहीं मिलता. अगर अपडेट बार-बार होते हैं और थ्रेड के बीच बदलते रहते हैं, तो हर बार शेयर किए गए काउंटर को अपडेट करने से बचना ज़्यादा तेज़ होता है. उदाहरण के लिए, थ्रेड-लोकल काउंटर का इस्तेमाल करना और आखिर में उन्हें जोड़ना.
  • इस तकनीक को पिछले सेक्शन के साथ जोड़ा जा सकता है: अपडेट की जा रही अनुमानित और अविश्वसनीय वैल्यू को एक साथ पढ़ा जा सकता है. इसके लिए, सभी कार्रवाइयों में memory_order_relaxed का इस्तेमाल किया जाता है. हालांकि, यह ज़रूरी है कि मिलने वाली वैल्यू को पूरी तरह से अविश्वसनीय माना जाए. सिर्फ़ इसलिए कि संख्या एक बार बढ़ाई गई है, इसका मतलब यह नहीं है कि किसी दूसरे थ्रेड को उस पॉइंट तक पहुंचने के लिए गिना जा सकता है जिस पर बढ़ोतरी की गई है. इसके बजाय, हो सकता है कि इंक्रीमेंट को पहले के कोड के साथ फिर से क्रम में लगाया गया हो. (जैसा कि हमने पहले बताया था, C++ यह गारंटी देता है कि ऐसे काउंटर का दूसरा लोड, उसी थ्रेड में पहले लोड से कम वैल्यू नहीं दिखाएगा. जब तक कि काउंटर ओवरफ़्लो न हो जाए.)
  • ऐसा कोड मिलना आम बात है जो अलग-अलग ऐटॉमिक (या नहीं) रीड और राइट करके, काउंटर की अनुमानित वैल्यू को कैलकुलेट करने की कोशिश करता है. हालांकि, यह बढ़ोतरी को पूरी तरह से ऐटॉमिक नहीं बनाता. आम तौर पर यह तर्क दिया जाता है कि परफ़ॉर्मेंस काउंटर या इस तरह की चीज़ों के लिए, यह "काफ़ी करीब" है. आम तौर पर, ऐसा नहीं होता. जब अपडेट काफ़ी बार होते हैं (ऐसा मामला जिसकी आपको शायद चिंता हो), तो आम तौर पर गिनती का एक बड़ा हिस्सा खो जाता है. क्वाड कोर डिवाइस पर, आम तौर पर आधे से ज़्यादा काउंट नहीं दिखते. (आसान एक्सरसाइज़: दो थ्रेड वाला ऐसा सिनेरियो बनाएं जिसमें काउंटर को दस लाख बार अपडेट किया गया हो, लेकिन काउंटर की आखिरी वैल्यू एक हो.)

फ़्लैग करने का आसान तरीका

memory_order_release स्टोर (या रीड-मॉडिफ़ाई-राइट ऑपरेशन) यह पक्का करता है कि अगर बाद में memory_order_acquire लोड (या रीड-मॉडिफ़ाई-राइट ऑपरेशन) लिखी गई वैल्यू को पढ़ता है, तो वह memory_order_release स्टोर से पहले के किसी भी स्टोर (सामान्य या ऐटॉमिक) को भी देखेगा. इसके उलट, memory_order_release से पहले लोड किए गए किसी भी डेटा में, memory_order_acquire के बाद लोड किए गए डेटा का कोई भी स्टोर नहीं दिखेगा. memory_order_relaxed के उलट, यह ऐसे ऐटॉमिक ऑपरेशन को अनुमति देता है जिससे एक थ्रेड की प्रोग्रेस को दूसरे थ्रेड को बताया जा सके.

उदाहरण के लिए, हम ऊपर दिए गए डबल-चेक लॉकिंग के उदाहरण को C++ में इस तरह से फिर से लिख सकते हैं

class MyClass {
  private:
    atomic<Helper*> helper {nullptr};
    mutex mtx;
  public:
    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper == nullptr) {
        lock_guard<mutex> lg(mtx);
        myHelper = helper.load(memory_order_relaxed);
        if (myHelper == nullptr) {
          myHelper = new Helper();
          helper.store(myHelper, memory_order_release);
        }
      }
      return myHelper;
    }
};

अक्वायर लोड और रिलीज़ स्टोर यह पक्का करते हैं कि अगर हमें कोई नॉन-नुल helper दिखता है, तो हमें उसके फ़ील्ड भी सही तरीके से शुरू किए गए दिखेंगे. हमने पहले के इस ऑब्ज़र्वेशन को भी शामिल किया है कि नॉन-रेसिंग लोड के लिए memory_order_relaxed का इस्तेमाल किया जा सकता है.

कोई Java प्रोग्रामर, helper को java.util.concurrent.atomic.AtomicReference<Helper> के तौर पर दिखा सकता है और lazySet() को रिलीज़ स्टोर के तौर पर इस्तेमाल कर सकता है. लोड ऑपरेशन के लिए, सामान्य get() कॉल का इस्तेमाल जारी रहेगा.

दोनों ही मामलों में, परफ़ॉर्मेंस में किए गए बदलावों का असर, इनिशलाइज़ेशन पाथ पर पड़ा. हालांकि, इससे परफ़ॉर्मेंस पर कोई खास असर नहीं पड़ा. ज़्यादा आसानी से पढ़ा जा सकने वाला विकल्प यह हो सकता है:

    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper != nullptr) {
        return myHelper;
      }
      lock_guard&ltmutex> lg(mtx);
      if (helper == nullptr) {
        helper = new Helper();
      }
      return helper;
    }

यह वही तेज़ पाथ उपलब्ध कराता है. हालांकि, यह परफ़ॉर्मेंस के लिए ज़रूरी नहीं होने वाले धीमे पाथ पर, डिफ़ॉल्ट और क्रम के हिसाब से एक जैसे ऑपरेशन का इस्तेमाल करता है.

यहां भी, helper.load(memory_order_acquire), मौजूदा Android-सपोर्टेड आर्किटेक्चर पर वही कोड जनरेट कर सकता है जो helper के लिए एक सामान्य (क्रम के हिसाब से एक जैसा) रेफ़रंस है. यहां सबसे फ़ायदेमंद ऑप्टिमाइज़ेशन, myHelper को शामिल करना हो सकता है. इससे दूसरी बार लोड करने की ज़रूरत नहीं पड़ती. हालांकि, आने वाले समय में कंपाइलर ऐसा अपने-आप कर सकता है.

Acquire/release ऑर्डरिंग, स्टोर को साफ़ तौर पर दिखने से नहीं रोकती देरी हो रही है और यह पक्का नहीं करता कि स्टोर अन्य थ्रेड को दिखें एक जैसा क्रम. इस वजह से, यह एक मुश्किल, लेकिन काफ़ी आम कोडिंग पैटर्न के साथ काम नहीं करता. इस पैटर्न को डेकर के म्यूचल एक्सक्लूज़न एल्गोरिदम के उदाहरण से समझा जा सकता है: सभी थ्रेड पहले एक फ़्लैग सेट करते हैं, जिससे पता चलता है कि वे कुछ करना चाहते हैं; अगर कोई थ्रेड t यह देखता है कि कोई दूसरा थ्रेड कुछ करने की कोशिश नहीं कर रहा है, तो वह सुरक्षित तरीके से आगे बढ़ सकता है. उसे पता होता है कि कोई रुकावट नहीं आएगी. कोई अन्य थ्रेड आगे नहीं बढ़ पाएगा, क्योंकि t का फ़्लैग अब भी सेट है. अगर फ़्लैग को ऐक्सेस करने के लिए, acquire/release ऑर्डर का इस्तेमाल किया जाता है, तो यह तरीका काम नहीं करता. ऐसा इसलिए होता है, क्योंकि यह तरीका किसी थ्रेड के फ़्लैग को दूसरों को देर से दिखने से नहीं रोकता. ऐसा तब होता है, जब वे गलती से आगे बढ़ जाते हैं. डिफ़ॉल्ट memory_order_seq_cst इसे रोकता है.

बदले न जा सकने वाले फ़ील्ड

अगर किसी ऑब्जेक्ट फ़ील्ड को पहली बार इस्तेमाल करने पर शुरू किया जाता है और फिर कभी नहीं बदला जाता, तो उसे शुरू किया जा सकता है. इसके बाद, कमज़ोर क्रम वाले ऐक्सेस का इस्तेमाल करके उसे पढ़ा जा सकता है. C++ में, इसे atomic के तौर पर एलान किया जा सकता है और memory_order_relaxed का इस्तेमाल करके ऐक्सेस किया जा सकता है. इसके अलावा, Java में इसे volatile के बिना एलान किया जा सकता है और बिना किसी खास तरीके के ऐक्सेस किया जा सकता है. इसके लिए, ये सभी शर्तें पूरी होनी चाहिए:

  • फ़ील्ड की वैल्यू से यह पता चलना चाहिए कि उसे पहले ही शुरू किया जा चुका है या नहीं. फ़ील्ड को ऐक्सेस करने के लिए, फ़ास्ट पाथ टेस्ट-ऐंड-रिटर्न वैल्यू को फ़ील्ड को सिर्फ़ एक बार पढ़ना चाहिए. जावा में, बाद वाला तरीका ज़रूरी है. अगर फ़ील्ड टेस्ट में यह दिखता है कि फ़ील्ड को शुरू कर दिया गया है, तो भी हो सकता है कि दूसरी बार लोड होने पर, पहले से शुरू नहीं की गई वैल्यू को पढ़ा जाए. C++ में, "एक बार पढ़ें" नियम सिर्फ़ एक अच्छी प्रैक्टिस है.
  • शुरुआती और बाद के लोड, दोनों ही ऐटॉमिक होने चाहिए. इसका मतलब है कि अधूरे अपडेट नहीं दिखने चाहिए. Java के लिए, फ़ील्ड long या double नहीं होना चाहिए. C++ के लिए, एक ऐटॉमिक असाइनमेंट ज़रूरी है. इसे उसी जगह पर बनाने से काम नहीं चलेगा, क्योंकि atomic का निर्माण ऐटॉमिक नहीं है.
  • बार-बार शुरू करना सुरक्षित होना चाहिए, क्योंकि कई थ्रेड, शुरू नहीं की गई वैल्यू को एक साथ पढ़ सकते हैं. C++ में, यह आम तौर पर सभी ऐटॉमिक टाइप के लिए लागू की गई "ट्रिवियली कॉपीएबल" शर्त के मुताबिक होता है. नेस्ट किए गए पॉइंटर वाले टाइप के लिए, कॉपी कंस्ट्रक्टर में डिएलोकेशन की ज़रूरत होगी. साथ ही, वे ट्रिवियली कॉपीएबल नहीं होंगे. Java के लिए, कुछ रेफ़रंस टाइप स्वीकार किए जाते हैं:
  • Java रेफ़रंस, सिर्फ़ ऐसे इम्यूटेबल टाइप तक सीमित हैं जिनमें सिर्फ़ फ़ाइनल फ़ील्ड शामिल होते हैं. इम्यूटेबल टाइप के कंस्ट्रक्टर को ऑब्जेक्ट का रेफ़रंस पब्लिश नहीं करना चाहिए. इस मामले में, Java के फ़ाइनल फ़ील्ड के नियम यह पक्का करते हैं कि अगर किसी रीडर को रेफ़रंस दिखता है, तो उसे शुरू किए गए फ़ाइनल फ़ील्ड भी दिखेंगे. C++ में इन नियमों के लिए कोई एनालॉग नहीं है. साथ ही, इस वजह से भी मालिकाना हक वाले ऑब्जेक्ट के पॉइंटर स्वीकार नहीं किए जाते. इसके अलावा, "आसानी से कॉपी किए जा सकने वाले" नियमों का उल्लंघन भी होता है.

क्लोज़िंग नोट

हालांकि, इस दस्तावेज़ में सिर्फ़ बुनियादी जानकारी नहीं दी गई है, लेकिन इसमें ज़्यादा गहराई से भी जानकारी नहीं दी गई है. यह एक बहुत ही बड़ा और गहरा विषय है. इन विषयों के बारे में और जानें:

  • असल Java और C++ मेमोरी मॉडल को happens-before रिलेशन के तौर पर दिखाया जाता है. इससे यह तय होता है कि दो कार्रवाइयां किसी खास क्रम में कब होंगी. जब हमने डेटा रेस को परिभाषित किया, तो हमने अनौपचारिक रूप से दो मेमोरी एक्सेस के "एक साथ" होने के बारे में बात की. आधिकारिक तौर पर, इसका मतलब है कि दोनों में से कोई भी इवेंट, दूसरे से पहले नहीं होता. Java या C++ मेमोरी मॉडल में, happens-before और synchronizes-with की असल परिभाषाओं को समझना ज़रूरी है. आम तौर पर, "एक साथ" होने की सहज धारणा काफ़ी अच्छी होती है. हालांकि, ये परिभाषाएं काफ़ी मददगार होती हैं. खास तौर पर, तब जब आपको C++ में कम क्रम वाले ऐटॉमिक ऑपरेशन इस्तेमाल करने हों. फ़िलहाल, Java की खास जानकारी में lazySet() को सिर्फ़ अनौपचारिक तौर पर परिभाषित किया गया है.
  • जानें कि कोड को फिर से क्रम में लगाते समय, कंपाइलर को क्या करने की अनुमति है और क्या नहीं. (JSR-133 स्पेसिफ़िकेशन में, कानूनी तौर पर किए जाने वाले बदलावों के कुछ बेहतरीन उदाहरण दिए गए हैं. इनकी वजह से, अनचाहे नतीजे मिलते हैं.)
  • Java और C++ में इम्यूटेबल क्लास लिखने का तरीका जानें. (इसमें “क्लास बनाने के बाद कुछ भी न बदलें” के अलावा और भी बहुत कुछ है.)
  • Effective Java, 2nd Edition के Concurrency सेक्शन में दिए गए सुझावों को समझें. उदाहरण के लिए, आपको ऐसे तरीके इस्तेमाल करने से बचना चाहिए जिन्हें सिंक किए गए ब्लॉक में ओवरराइड किया जाना है.
  • java.util.concurrent और java.util.concurrent.atomic एपीआई के बारे में पढ़ें और देखें कि क्या उपलब्ध है. @ThreadSafe और @GuardedBy (net.jcip.annotations से) जैसे कॉनकरंसी एनोटेशन का इस्तेमाल करें.

अपेंडिक्स में मौजूद ज़्यादा जानकारी सेक्शन में, उन दस्तावेज़ों और वेबसाइटों के लिंक दिए गए हैं जिनसे आपको इन विषयों के बारे में ज़्यादा जानकारी मिलेगी.

अपेंडिक्स

सिंक्रनाइज़ेशन स्टोर लागू करना

(ज़्यादातर प्रोग्रामर को इसे लागू करने की ज़रूरत नहीं पड़ती, लेकिन इस पर चर्चा करना ज़रूरी है.)

int जैसे छोटे बिल्ट-इन टाइप और Android के साथ काम करने वाले हार्डवेयर के लिए, लोड और स्टोर करने के सामान्य निर्देशों से यह पक्का होता है कि स्टोर, उसी जगह को लोड करने वाले किसी दूसरे प्रोसेसर को पूरी तरह से दिखेगा या बिलकुल नहीं दिखेगा. इस तरह, "ऐटमिसिटी" का कुछ बुनियादी सिद्धांत बिना किसी शुल्क के उपलब्ध कराया जाता है.

जैसा कि हमने पहले देखा, यह काफ़ी नहीं है. क्रम के मुताबिक एक जैसा अनुभव देने के लिए, हमें कार्रवाइयों के क्रम को बदलने से भी रोकना होगा. साथ ही, यह पक्का करना होगा कि मेमोरी से जुड़ी कार्रवाइयां, अन्य प्रोसेस को एक जैसे क्रम में दिखें. यह पता चला है कि Android पर काम करने वाले हार्डवेयर पर, बाद वाला विकल्प अपने-आप लागू हो जाता है. हालांकि, इसके लिए ज़रूरी है कि हम पहले विकल्प को लागू करने के लिए सही फ़ैसले लें. इसलिए, हम यहां इसे ज़्यादातर अनदेखा करते हैं.

मेमोरी ऑपरेशन के क्रम को सुरक्षित रखा जाता है. इसके लिए, कंपाइलर और हार्डवेयर, दोनों को क्रम बदलने से रोका जाता है. यहां हम दूसरे तरीके पर फ़ोकस करेंगे.

ARMv7, x86, और MIPS पर मेमोरी ऑर्डरिंग को "फ़ेंस" निर्देशों के साथ लागू किया जाता है. ये निर्देश, फ़ेंस के बाद आने वाले निर्देशों को फ़ेंस से पहले आने वाले निर्देशों से पहले दिखने से रोकते हैं. (इन्हें आम तौर पर "बैरियर" निर्देश भी कहा जाता है. हालांकि, इससे pthread_barrier-स्टाइल वाले बैरियर के साथ भ्रम होने का खतरा रहता है, जो इससे कहीं ज़्यादा काम करते हैं.) फ़ेंस निर्देशों का सटीक मतलब काफ़ी जटिल विषय है. इसमें यह बताना होता है कि अलग-अलग तरह के फ़ेंस से मिलने वाली गारंटी किस तरह इंटरैक्ट करती हैं. साथ ही, यह भी बताना होता है कि ये गारंटी, आम तौर पर हार्डवेयर से मिलने वाली ऑर्डर की अन्य गारंटी के साथ कैसे जुड़ती हैं. यह एक खास जानकारी है, इसलिए हम इन जानकारी को संक्षेप में बताएंगे.

ऑर्डर करने की सबसे बुनियादी गारंटी, C++ memory_order_acquire और memory_order_release ऐटॉमिक ऑपरेशन से मिलती है: रिलीज़ स्टोर से पहले होने वाले मेमोरी ऑपरेशन, अक्वायर लोड के बाद दिखने चाहिए. ARMv7 पर, इसे लागू करने के लिए ज़रूरी है:

  • स्टोर निर्देश से पहले, सही फ़ेंस निर्देश का इस्तेमाल करना. इससे, मेमोरी के सभी पिछले ऐक्सेस को स्टोर निर्देश के साथ फिर से क्रम में लगाने से रोका जाता है. (यह बाद में स्टोर के निर्देश के साथ, फिर से ऑर्डर करने से भी रोकता है.)
  • लोड करने के निर्देश के बाद, फ़ेंस का सही निर्देश दें. इससे, लोड करने के निर्देश को बाद के ऐक्सेस के साथ फिर से क्रम में लगाने से रोका जा सकता है. (साथ ही, कम से कम पहले लोड किए गए कॉन्टेंट के लिए, फिर से गैर-ज़रूरी ऑर्डरिंग की सुविधा दी जा रही है.)

ये दोनों मिलकर, C++ के लिए एक्वायर/रिलीज़ ऑर्डरिंग को पूरा करते हैं. ये ज़रूरी हैं, लेकिन Java volatile या C++ के लिए, क्रम से लगातार atomic के लिए काफ़ी नहीं हैं.

हमें और क्या चाहिए, यह देखने के लिए डेकर के एल्गोरिदम के उस हिस्से पर विचार करें जिसके बारे में हमने पहले संक्षेप में बताया था. flag1 और flag2, C++ atomic या Java volatile वैरिएबल हैं. दोनों की शुरुआती वैल्यू false है.

थ्रेड 1 थ्रेड 2
flag1 = true
if (flag2 == false)
    critical-stuff
flag2 = true
if (flag1 == false)
    critical-stuff

क्रम के मुताबिक एक जैसा होने का मतलब है कि flagn को असाइन किए गए टास्क में से किसी एक को पहले पूरा करना होगा. साथ ही, उसे दूसरे थ्रेड में टेस्ट के ज़रिए देखा जाना चाहिए. इसलिए, हम इन थ्रेड को एक साथ “क्रिटिकल-स्टफ़” को एक्ज़ीक्यूट करते हुए कभी नहीं देखेंगे.

हालांकि, acquire-release ऑर्डर के लिए ज़रूरी फ़ेंसिंग, हर थ्रेड की शुरुआत और आखिर में ही फ़ेंस जोड़ती है. इससे यहां कोई मदद नहीं मिलती. हमें यह भी पक्का करना होगा कि अगर volatile/atomic स्टोर के बाद volatile/atomic लोड होता है, तो दोनों का क्रम न बदला जाए. आम तौर पर, इसे लागू करने के लिए, क्रम से चलने वाले स्टोर से पहले और बाद में एक फ़ेंस जोड़ा जाता है. (यह फिर से ज़रूरत से ज़्यादा मज़बूत है, क्योंकि यह फ़ेंस आम तौर पर बाद के सभी मेमोरी ऐक्सेस के हिसाब से, पहले के सभी मेमोरी ऐक्सेस को क्रम में लगाता है.)

इसके बजाय, हम अतिरिक्त फ़ेंस को क्रम से चलने वाले लोड के साथ जोड़ सकते हैं. स्टोर कम होते हैं, इसलिए हमने जो तरीका बताया है वह ज़्यादा आम है और Android पर इस्तेमाल किया जाता है.

जैसा कि हमने पिछले सेक्शन में देखा था, हमें दोनों कार्रवाइयों के बीच एक स्टोर/लोड बैरियर डालना होगा. अस्थिर ऐक्सेस के लिए, वीएम में चलाया गया कोड कुछ इस तरह दिखेगा:

वोलेटाइल लोड वोलेटाइल स्टोर
reg = A
fence for "acquire" (1)
fence for "release" (2)
A = reg
fence for later atomic load (3)

असल मशीन आर्किटेक्चर में आम तौर पर कई तरह के फ़ेंस होते हैं. ये अलग-अलग तरह के ऐक्सेस को क्रम में लगाते हैं और इनकी लागत अलग-अलग हो सकती है. इनके बीच का अंतर बहुत कम है. साथ ही, यह इस बात पर निर्भर करता है कि स्टोर को अन्य कोर को एक जैसे क्रम में दिखाया जाए. साथ ही, कई फ़ेंस के कॉम्बिनेशन से लागू किया गया मेमोरी ऑर्डर सही तरीके से कंपोज़ किया जाए. ज़्यादा जानकारी के लिए, कृपया कैंब्रिज यूनिवर्सिटी का पेज देखें. इसमें असल प्रोसेसर के लिए, ऐटॉमिक्स की इकट्ठा की गई मैपिंग मौजूद है.

कुछ आर्किटेक्चर पर, खास तौर पर x86 पर, "हासिल करें" और "रिलीज़ करें" बैरियर की ज़रूरत नहीं होती, क्योंकि हार्डवेयर हमेशा सही क्रम में लागू होता है. इसलिए, x86 पर सिर्फ़ आखिरी फ़ेंस (3) जनरेट होता है. इसी तरह, x86 पर ऐटॉमिक रीड-मॉडिफ़ाई-राइट ऑपरेशन में, इंप्लिसिट तौर पर एक मज़बूत फ़ेंस शामिल होता है. इसलिए, इन्हें कभी भी बाड़ लगाने की ज़रूरत नहीं पड़ती. ARMv7 पर, ऊपर बताए गए सभी फ़ेंस ज़रूरी हैं.

ARMv8, LDAR और STLR निर्देश देता है. ये निर्देश, Java वोलेटाइल या C++ के क्रम से लगातार लोड और स्टोर करने की ज़रूरी शर्तों को सीधे तौर पर लागू करते हैं. इनसे, ऊपर बताई गई फिर से क्रम में लगाने की पाबंदियों से बचा जा सकता है. ARM पर 64-बिट Android कोड इनका इस्तेमाल करता है; हमने यहां ARMv7 फ़ेंस प्लेसमेंट पर फ़ोकस करने का फ़ैसला लिया है, क्योंकि इससे असल ज़रूरतों के बारे में ज़्यादा जानकारी मिलती है.

इस बारे में और पढ़ें

ऐसे वेब पेज और दस्तावेज़ जिनमें ज़्यादा जानकारी दी गई हो. आम तौर पर, ज़्यादा काम के लेख सूची में सबसे ऊपर होते हैं.

शेयर की गई मेमोरी के कंसिस्टेंसी मॉडल: एक ट्यूटोरियल
इसे 1995 में अद्वे और घराचोरलू ने लिखा था. अगर आपको मेमोरी कंसिस्टेंसी मॉडल के बारे में ज़्यादा जानना है, तो यह एक अच्छी जगह है.
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf
मेमोरी बैरियर
समस्याओं के बारे में बताने वाला छोटा और अच्छा लेख.
https://en.wikipedia.org/wiki/Memory_barrier
Threads की बुनियादी जानकारी
हैंस बोएम की लिखी गई, C++ और Java में मल्टी-थ्रेडेड प्रोग्रामिंग के बारे में जानकारी. डेटा रेस और सिंक करने के बुनियादी तरीकों के बारे में चर्चा.
http://www.hboehm.info/c++mm/threadsintro.html
Java Concurrency In Practice
साल 2006 में पब्लिश हुई इस किताब में, कई विषयों के बारे में पूरी जानकारी दी गई है. यह किताब उन लोगों के लिए बहुत काम की है जो Java में मल्टी-थ्रेडेड कोड लिखते हैं.
http://www.javaconcurrencyinpractice.com
JSR-133 (Java Memory Model) के बारे में अक्सर पूछे जाने वाले सवाल
Java मेमोरी मॉडल के बारे में आसान शब्दों में जानकारी. इसमें सिंक्रनाइज़ेशन, वोलेटाइल वैरिएबल, और फ़ाइनल फ़ील्ड बनाने के बारे में भी बताया गया है. (यह थोड़ा पुराना है, खास तौर पर जब इसमें अन्य भाषाओं के बारे में बताया गया हो.)
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Java मेमोरी मॉडल में प्रोग्राम ट्रांसफ़ॉर्मेशन की वैधता
Java मेमोरी मॉडल की बाकी समस्याओं के बारे में तकनीकी जानकारी. ये समस्याएं, डेटा-रेस-फ़्री प्रोग्राम पर लागू नहीं होती हैं.
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf
पैकेज java.util.concurrent की खास जानकारी
java.util.concurrent पैकेज के लिए दस्तावेज़. पेज पर सबसे नीचे, “मेमोरी कंसिस्टेंसी प्रॉपर्टीज़” नाम का एक सेक्शन है. इसमें अलग-अलग क्लास की गारंटी के बारे में बताया गया है.
java.util.concurrent पैकेज की खास जानकारी
Java Theory and Practice: Safe Construction Techniques in Java
इस लेख में, ऑब्जेक्ट बनाने के दौरान रेफ़रंस के एस्केप होने के खतरों के बारे में विस्तार से बताया गया है. साथ ही, थ्रेड-सेफ़ कंस्ट्रक्टर के लिए दिशा-निर्देश दिए गए हैं.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html
Java Theory and Practice: Managing Volatility
इस लेख में बताया गया है कि Java में वोलेटाइल फ़ील्ड की मदद से क्या किया जा सकता है और क्या नहीं.
http://www.ibm.com/developerworks/java/library/j-jtp06197.html
“डबल-चेक्ड लॉकिंग काम नहीं कर रहा है” एलान
बिल प्यू ने उन अलग-अलग तरीकों के बारे में विस्तार से बताया है जिनसे volatile या atomic के बिना, डबल-चेक लॉकिंग टूट जाती है. इसमें C/C++ और Java शामिल हैं.
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[ARM] बैरियर लिटमस टेस्ट और कुकबुक
एआरएम एसएमपी से जुड़ी समस्याओं पर चर्चा, जिसमें एआरएम कोड के छोटे स्निपेट शामिल हैं. अगर आपको इस पेज पर दिए गए उदाहरण ज़्यादा सटीक नहीं लगे या आपको डीएमबी के निर्देश की औपचारिक जानकारी पढ़नी है, तो यह लेख पढ़ें. इसमें, एक्ज़ीक्यूटेबल कोड पर मेमोरी बैरियर के लिए इस्तेमाल किए जाने वाले निर्देशों के बारे में भी बताया गया है. अगर आपको तुरंत कोड जनरेट करना है, तो यह जानकारी आपके काम आ सकती है. ध्यान दें कि यह ARMv8 से पहले का है. इसमें मेमोरी ऑर्डर करने के अतिरिक्त निर्देश भी काम करते हैं. साथ ही, यह कुछ हद तक बेहतर मेमोरी मॉडल पर काम करता है. (ज़्यादा जानकारी के लिए, "ARMv8-A आर्किटेक्चर प्रोफ़ाइल के लिए ARM® आर्किटेक्चर रेफ़रंस मैन्युअल ARMv8" देखें.)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf
Linux Kernel Memory Barriers
Linux कर्नेल मेमोरी बैरियर के लिए दस्तावेज़. इसमें कुछ काम के उदाहरण और ASCII आर्ट शामिल है.
http://www.kernel.org/doc/Documentation/memory-barriers.txt
ISO/IEC JTC1 SC22 WG21 (C++ स्टैंडर्ड) 14882 (C++ प्रोग्रामिंग लैंग्वेज), सेक्शन 1.10 और क्लॉज़ 29 (“ऐटॉमिक ऑपरेशंस लाइब्रेरी”)
C++ ऐटॉमिक ऑपरेशन की सुविधाओं के लिए ड्राफ़्ट स्टैंडर्ड. यह वर्शन, C++14 स्टैंडर्ड के काफ़ी करीब है. इसमें C++11 के मुकाबले, इस सेक्शन में कुछ छोटे-मोटे बदलाव किए गए हैं.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(परिचय: http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf)
ISO/IEC JTC1 SC22 WG14 (C स्टैंडर्ड) 9899 (C प्रोग्रामिंग लैंग्वेज) चैप्टर 7.16 (“Atomics <stdatomic.h>”)
ISO/IEC 9899-201x C के ऐटॉमिक ऑपरेशन की सुविधाओं के लिए ड्राफ़्ट स्टैंडर्ड. ज़्यादा जानकारी के लिए, बाद में आने वाली गड़बड़ियों की रिपोर्ट भी देखें.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
C/C++11 mappings to processors (University of Cambridge)
Jaroslav Sevcik और Peter Sewell का C++ ऐटॉमिक्स के अनुवादों का कलेक्शन, जो प्रोसेसर के लिए दिशा-निर्देशों के अलग-अलग सेट के लिए है.
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
डेकर का एल्गोरिदम
“कॉन्करेंट प्रोग्रामिंग में, म्यूचल एक्सक्लूज़न की समस्या का पहला सही समाधान”. विकिपीडिया के लेख में पूरा एल्गोरिदम मौजूद है. साथ ही, इसमें इस बारे में चर्चा की गई है कि आधुनिक ऑप्टिमाइज़िंग कंपाइलर और एसएमपी हार्डवेयर के साथ काम करने के लिए, इसे कैसे अपडेट करना होगा.
https://en.wikipedia.org/wiki/Dekker's_algorithm
एआरएम बनाम ऐल्फ़ा और पते की डिपेंडेंसी पर टिप्पणियां
कैटलिन मरीनास का arm-kernel मेलिंग सूची पर भेजा गया एक ईमेल. इसमें पते और कंट्रोल डिपेंडेंसी की अच्छी खास जानकारी शामिल है.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html
What Every Programmer Should Know About Memory
उलरिच ड्रेपर का लिखा हुआ एक बहुत लंबा और ज़्यादा जानकारी वाला लेख. इसमें अलग-अलग तरह की मेमोरी, खास तौर पर सीपीयू कैश के बारे में बताया गया है.
http://www.akkadia.org/drepper/cpumemory.pdf
एआरएम के वीकली कंसिस्टेंट मेमोरी मॉडल के बारे में तर्क
इस पेपर को ARM, Ltd. के चोंग और इश्तियाक ने लिखा है. इसमें ARM एसएमपी मेमोरी मॉडल के बारे में सटीक और आसान तरीके से बताया गया है. यहां इस्तेमाल की गई “जांचने की क्षमता” की परिभाषा, इस पेपर से ली गई है. यह भी ARMv8 से पहले का है.
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll=&dl=&CFID=96099715&CFTOKEN=57505711
The JSR-133 Cookbook for Compiler Writers
डग ली ने इसे JSR-133 (Java Memory Model) दस्तावेज़ के साथ लिखा है. इसमें Java मेमोरी मॉडल को लागू करने के लिए, शुरुआती दिशा-निर्देशों का सेट शामिल है. इसका इस्तेमाल, कंपाइलर लिखने वाले कई लोगों ने किया था. इसे अब भी बड़े पैमाने पर इस्तेमाल किया जाता है. इससे अहम जानकारी मिलने की संभावना है. माफ़ करें, यहां बताए गए चार तरह के फ़ेंस, Android के साथ काम करने वाले आर्किटेक्चर के लिए सही नहीं हैं. साथ ही, ऊपर दिए गए C++11 मैपिंग, अब सटीक रेसिपी के बेहतर सोर्स हैं. यहां तक कि Java के लिए भी.
http://g.oswego.edu/dl/jmm/cookbook.html
x86-TSO: A Rigorous and Usable Programmer’s Model for x86 Multiprocessors
x86 मेमोरी मॉडल के बारे में सटीक जानकारी. एआरएम मेमोरी मॉडल के बारे में सटीक जानकारी देना, काफ़ी मुश्किल है.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf