Android 3.0 और इसके बाद के वर्शन, मल्टीप्रोसेसर आर्किटेक्चर के साथ काम करने के लिए ऑप्टिमाइज़ किए गए हैं. इस दस्तावेज़ में ऐसी समस्याओं के बारे में बताया गया है जो C, C++, और Java में सिमेट्रिक मल्टीप्रोसेसर सिस्टम के लिए मल्टीथ्रेड कोड लिखते समय यह आ सकता है (इसके बाद, इसे सिर्फ़ “Java” कहा जाता है. कम शब्दों में दी जाने वाली जानकारी. इसे Android ऐप्लिकेशन डेवलपर के लिए, शुरुआती जानकारी देने के मकसद से बनाया गया है चर्चा करें.
परिचय
एसएमपी, “सिमेट्रिक मल्टी-प्रोसेसर” का छोटा नाम है. यह ऐसी डिज़ाइन के बारे में बताता है जिसमें कौनसे दो या उससे ज़्यादा एक जैसे सीपीयू कोर, मुख्य मेमोरी का ऐक्सेस शेयर करते हैं. पूरा होने तक कुछ साल पहले, सभी Android डिवाइस UP (Uni-प्रोसेसर) थे.
ज़्यादातर — अगर सभी नहीं — Android डिवाइसों में हमेशा एक से ज़्यादा सीपीयू होते थे, लेकिन पहले उनमें से सिर्फ़ एक का इस्तेमाल ऐप्लिकेशन चलाने के लिए किया जाता था, जबकि दूसरे ऐप्लिकेशन के अलग-अलग बिट को मैनेज करते थे हार्डवेयर (उदाहरण के लिए, रेडियो). हो सकता है कि सीपीयू के आर्किटेक्चर अलग-अलग हों और उन पर चलने वाले प्रोग्राम, एक-दूसरे के साथ कम्यूनिकेट करने के लिए मुख्य मेमोरी का इस्तेमाल न कर पाएं.
आज-कल बिकने वाले ज़्यादातर Android डिवाइस, एसएमपी डिज़ाइन के हिसाब से बनाए जाते हैं, इससे सॉफ़्टवेयर डेवलपर के लिए चीज़ें बनाना थोड़ा और मुश्किल हो जाता है. रेस की शर्तें हो सकता है कि मल्टी-थ्रेड वाले प्रोग्राम में यूनिप्रोसेसर पर दिखाई देने वाली समस्याएं न हों, हालांकि, हो सकता है कि आपकी दो या उससे ज़्यादा थ्रेड में, समय-समय पर गड़बड़ी हो जाए एक साथ अलग-अलग कोर पर चल रहे हों. इसके अलावा, अलग-अलग प्रोसेसर आर्किटेक्चर पर चलाए जाने पर, कोड काम न कर सकता है. इसके अलावा, एक ही आर्किटेक्चर के अलग-अलग तरीके से लागू होने पर भी ऐसा हो सकता है. x86 पर पूरी तरह से टेस्ट किया गया कोड, ARM पर काम नहीं कर सकता. किसी नए कंपाइलर की मदद से फिर से कंपाइल करने पर, कोड काम करना बंद कर सकता है.
इस दस्तावेज़ के बाकी हिस्से में इसकी वजह बताई गई है. साथ ही, यह भी बताया गया है कि आगे आपको क्या करना होगा ताकि यह पक्का किया जा सके कि आपका कोड सही तरीके से काम कर रहा है.
मेमोरी कंसिस्टेंसी मॉडल: एसएमपी थोड़े अलग क्यों हैं
यह एक मुश्किल विषय की तेज़ी से और पूरी जानकारी देने के लिए है. कुछ चीज़ों में अधूरा न हो, लेकिन कोई भी जानकारी गुमराह करने वाली या गलत न हो. अगर आपने अगले सेक्शन में दिखेगा. आम तौर पर, यहां दी गई जानकारी अहम नहीं होती.
इसके लिए दस्तावेज़ के अंत में आगे पढ़ें की तरफ़ इशारा करता है.
मेमोरी के एक जैसे होने वाले मॉडल या अक्सर सिर्फ़ “मेमोरी मॉडल” में, प्रोग्रामिंग भाषा या हार्डवेयर आर्किटेक्चर की गारंटी देता है मेमोरी के ऐक्सेस के बारे में बताती है. उदाहरण के लिए, अगर आपने पता A के लिए कोई वैल्यू लिखी है और फिर पता B के लिए कोई वैल्यू लिखी है, तो मॉडल यह पक्का कर सकता है कि हर सीपीयू कोर को वे वैल्यू उसी क्रम में दिखें.
ज़्यादातर प्रोग्रामर क्रम से होने वाली एक जैसी कार्रवाइयों वाले मॉडल का इस्तेमाल करते हैं. इस मॉडल के बारे में (Adve & Gharachorloo) इस तरह बताया गया है:
- ऐसा लगता है कि मेमोरी से जुड़ी सभी कार्रवाइयां एक-एक करके करती हैं
- एक थ्रेड में सभी कार्रवाइयां बताए गए क्रम में लागू होती हैं प्रोसेसर के प्रोग्राम से हो सकता है.
मान लें कि हमारे पास एक बहुत ही आसान कंपाइलर या इंटरप्रेटर है, जिसमें कोई समस्या नहीं है: यह सोर्स कोड में असाइनमेंट का अनुवाद करता है, ताकि निर्देशों को ठीक उसी क्रम में लोड और स्टोर किया जा सके. हर ऐक्सेस के लिए एक निर्देश. हम यह भी मानेंगे कि वह आसानी से काम करती है जिसे हर थ्रेड अपने प्रोसेसर पर एक्ज़ीक्यूट करता है.
अगर किसी कोड को देखने पर पता चलता है कि वह मेमोरी से कुछ पढ़ता और लिखता है, तो सीपीयू के क्रम से चलने वाले आर्किटेक्चर पर आपको पता चलता है कि कोड, उन चीज़ों को पढ़ने और लिखने का काम तय किए गए क्रम में करेगा. ऐसा हो सकता है कि सीपीयू असल में, निर्देशों का क्रम बदल रहा है और पढ़ने और लिखने में देरी कर रहा है, लेकिन असल में डिवाइस पर चलने वाले कोड के लिए यह बताने का कोई तरीका नहीं है कि सीपीयू कुछ कर रहा है उन पर आसान तरीके से कोई कार्रवाई नहीं की जा सकती. (हम मेमोरी-मैप किया गया डिवाइस ड्राइवर I/O.)
इन बातों को समझाने के लिए, कोड के छोटे स्निपेट जोड़ना, इन्हें आम तौर पर लिटमस टेस्ट कहा जाता है.
यहां एक आसान उदाहरण दिया गया है, जिसमें कोड दो थ्रेड पर चल रहा है:
थ्रेड 1 | थ्रेड 2 |
---|---|
A = 3 |
reg0 = B |
इस और आने वाले समय के सभी लिटमस उदाहरणों में, मेमोरी लोकेशन को बड़े अक्षरों (A, B, C) से दिखाया जाता है और सीपीयू रजिस्टर “reg” से शुरू होते हैं. शुरुआत में, सभी मेमोरी शून्य होती है. निर्देश ऊपर से नीचे की ओर लागू होते हैं. यहां, थ्रेड 1 वैल्यू 3 को जगह A पर सेव करता है. इसके बाद, वैल्यू 5 को जगह B पर सेव करता है. थ्रेड 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 को क्रम में लगाए गए निर्देश. हालांकि, अगर हमारा कंपाइलर फिर से ऑर्डर नहीं करता है, तो हम कभी भी इस समस्या का आकलन नहीं करना चाहिए. ज़्यादातर ARM एसएमपी पर, कंपाइलर के बिना भी फिर से ऑर्डर करते समय, शायद यह देखा जाए कि सफल निष्पादनों की संख्या. जब तक कि आप असेंबली में प्रोग्रामिंग न कर रहे हों भाषा, एसएमपी आम तौर पर इस बात की संभावना को और बढ़ा देते हैं कि आपको वे समस्याएं दिखाई दें जो साथ-साथ.
डेटा-रेस-फ़्री प्रोग्रामिंग
अच्छी बात यह है कि आम तौर पर, इनमें से किसी भी जानकारी के बारे में सोचने से बचा जा सकता है. अगर आप कुछ आसान नियमों का पालन करते हैं, तो आम तौर पर ऐसा करना सुरक्षित होता है "क्रम से लगाए गए एक जैसे फ़ंक्शन" को छोड़कर, पिछले सभी सेक्शन को भूल जाने के लिए . माफ़ करें, अन्य Android विजेट आपके लिए उपलब्ध हो सकते हैं, अगर गलती से उन नियमों का उल्लंघन नहीं होता.
आधुनिक प्रोग्रामिंग भाषाएं, "डेटा-रेस-फ़्री" प्रोग्रामिंग स्टाइल को बढ़ावा देती हैं. जब तक यह वादा न किया जाए कि "डेटा के रेस" लागू नहीं होंगे, कुछ ऐसे कंस्ट्रक्ट से बचें जो कंपाइलर को अलग तरीके से बताते हैं, कंपाइलर और हार्डवेयर, लगातार एक जैसे नतीजे देने का वादा करता है. ऐसा नहीं होता है इसका मतलब है कि वे मेमोरी ऐक्सेस को फिर से क्रम में लगाने से बचते हैं. इसका मतलब यह है कि अगर उन नियमों का पालन करें जो आपको यह नहीं बता पाएंगे कि मेमोरी ऐक्सेस फिर से क्रम में लगाया गया. यह ठीक वैसे ही है जैसे आपको यह बताना होता है कि सॉसेज बहुत स्वादिष्ट है और भूख लगने वाला खाना, जब तक कि आप यहां नहीं जाने का वादा करते हैं सॉसेज फ़ैक्ट्री. डेटा की रेस ही मेमोरी के बारे में भद्दी सच को सामने लाती हैं फिर से ऑर्डर करना.
"डेटा रेस" क्या है?
डेटा रेस तब होती है, जब कम से कम दो थ्रेड एक साथ ऐक्सेस करते हैं एक समान साधारण डेटा होता है और उनमें से कम से कम एक उसे संशोधित करता है. "सामान्य डेटा" हमारा मतलब कुछ ऐसा है जो खास तौर पर कोई सिंक्रोनाइज़ेशन ऑब्जेक्ट नहीं है का इस्तेमाल थ्रेड कम्यूनिकेशन के लिए किया जाता है. म्यूटेक्स, कंडीशन वैरिएबल, Java के वैरिएबल या C++ के एटमिक ऑब्जेक्ट सामान्य डेटा नहीं होते. साथ ही, इनके ऐक्सेस को रेस करने की अनुमति होती है. असल में, इनका इस्तेमाल अन्य ऑब्जेक्ट पर डेटा रेस को रोकने के लिए किया जाता है.
यह पता करने के लिए कि दो थ्रेड एक साथ एक ही को ऐक्सेस करते हैं या नहीं
मेमोरी की जगह की जानकारी है, तो हम ऊपर बताई गई मेमोरी को फिर से क्रम में लगाने वाली चर्चा को अनदेखा कर सकते हैं और
क्रम में एक जैसा होने का एहसास दिलाता है. इस प्रोग्राम में डेटा खर्च करने की कोई ज़रूरत नहीं है
अगर A
और B
साधारण बूलियन वैरिएबल हैं, जो
शुरुआत में गलत:
थ्रेड 1 | थ्रेड 2 |
---|---|
if (A) B = true |
if (B) A = true |
कार्रवाइयों का क्रम नहीं बदला जाता है. इसलिए, दोनों शर्तों का मान 'गलत' होगा, और
दोनों में से कोई भी वैरिएबल कभी अपडेट नहीं किया जाता. इसलिए, डेटा की रेस नहीं हो सकती. इस बात पर ध्यान देने की ज़रूरत नहीं है कि अगर थ्रेड 1 में A
से लोड करके B
में स्टोर करने के क्रम को किसी तरह से बदल दिया जाए, तो क्या होगा. कंपाइलर को Thread
1 को "B = true; if (!A) B = false
" के तौर पर फिर से लिखकर, उसका क्रम बदलने की अनुमति नहीं है. ऐसा करना,
साफ़ तौर पर ज़ाहिर होने वाली गतिविधि के तौर पर माना जाएगा.
डेटा रेस को आधिकारिक तौर पर, पहले से मौजूद बुनियादी टाइप के आधार पर तय किया जाता है. जैसे, पूर्णांक और रेफ़रंस या पॉइंटर. किसी int
को असाइन करते समय, उसे किसी दूसरी थ्रेड में पढ़ना, डेटा रेस है. हालांकि, C++ स्टैंडर्ड लाइब्रेरी और Java कलेक्शन लाइब्रेरी, दोनों को इस तरह से लिखा गया है कि लाइब्रेरी लेवल पर डेटा रेस के बारे में भी सोचा जा सके. वे डेटा रेस को तब तक शुरू नहीं करेंगे, जब तक एक ही कंटेनर को एक साथ ऐक्सेस नहीं किया जाता. साथ ही, उसमें से कम से कम एक कंटेनर को अपडेट किया जाता है. इस समय एक थ्रेड में set<T>
अपडेट किया जा रहा है
साथ-साथ किसी दूसरे पेज में पढ़ने से लाइब्रेरी को
डेटा रेस होती है और इसलिए अनौपचारिक तौर पर इसे "लाइब्रेरी लेवल की डेटा रेस" माना जा सकता है.
इसके उलट, एक थ्रेड में एक set<T>
को अपडेट करने और किसी दूसरी थ्रेड में किसी दूसरे set<T>
को पढ़ने पर, डेटा रेस नहीं होती. ऐसा इसलिए होता है, क्योंकि लाइब्रेरी इस बात का वादा करती है कि वह उस स्थिति में (लो-लेवल) डेटा रेस नहीं शुरू करेगी.
आम तौर पर, किसी डेटा स्ट्रक्चर में अलग-अलग फ़ील्ड को एक साथ ऐक्सेस किया जाता है डेटा रेस शुरू नहीं की जा सकती. हालांकि, विज्ञापन देने वालों के लिए, यह नियम: C या C++ में बिट-फ़ील्ड के निरंतर क्रम को एकल "मेमोरी स्थान". ऐसे क्रम में किसी भी बिट-फ़ील्ड को ऐक्सेस करना को उन सभी को ऐक्सेस करना माना जाता है जिनसे यह तय होता है कि की मौजूदगी. इससे पता चलता है कि सामान्य हार्डवेयर, आस-पास के बिट को पढ़े और फिर से लिखे बिना, अलग-अलग बिट को अपडेट नहीं कर सकता. Java प्रोग्रामर में इससे मिलती-जुलती कोई समस्या नहीं होती.
डेटा के उतार-चढ़ाव से बचना
मॉडर्न प्रोग्रामिंग भाषाएं कई तरह से सिंक करती हैं के तरीके इस्तेमाल करते हैं. सबसे बुनियादी टूल ये हैं:
- लॉक या म्यूटेक्स
- म्यूटेक्स (C++11
std::mutex
, याpthread_mutex_t
) या Java में मौजूदsynchronized
ब्लॉक का इस्तेमाल यह पक्का करने के लिए किया जा सकता है कि कुछ कोड का सेक्शन, कोड ऐक्सेस करने के अन्य सेक्शन के साथ एक साथ नहीं चलता एक ही डेटा है. हम इन और इससे मिलती-जुलती दूसरी सुविधाओं को सामान्य रूप से देखेंगे जैसे कि "लॉक". शेयर किए गए डेटा को ऐक्सेस करने से पहले, लगातार एक खास लॉक की सुविधा डेटा स्ट्रक्चर और उसे बाद में रिलीज़ करने से, ऐक्सेस करते समय डेटा के रेस से बचने में मदद मिलती है इस्तेमाल किया जा सकता है. इससे यह भी पक्का होता है कि ऐप्लिकेशन को अपडेट और ऐक्सेस करना थोड़ा ज़रूरी है. इसका मतलब है कि डेटा स्ट्रक्चर के अन्य अपडेट को बीच में चलाया जा सकता है. यह तो योग्य है यह अब तक का सबसे ज़्यादा इस्तेमाल किया जाने वाला टूल है. Javasynchronized
ब्लॉक या C++lock_guard
याunique_lock
का इस्तेमाल करने से, यह पक्का होता है कि किसी अपवाद की स्थिति में लॉक सही तरीके से रिलीज़ हो जाएं. - वोलाटाइल/ऐटॉमिक वैरिएबल
- Java
volatile
फ़ील्ड देता है जो एक साथ ऐक्सेस करने की सुविधा देता है वह भी कई तरह के डेटा की रेस शामिल किए बिना. C और C++ में, 2011 सेatomic
वैरिएबल और फ़ील्ड के साथ काम करने की सुविधा उपलब्ध है. ये हैं लॉक के मुकाबले, आम तौर पर इस्तेमाल करना ज़्यादा मुश्किल होता है, क्योंकि इनसे सिर्फ़ यह पक्का होता है कि किसी एक वैरिएबल का अलग-अलग ऐक्सेस, ऐटॉमिक होता है. (C++ में यह सामान्य तौर पर होता है पढ़ने-लिखने में बदलाव करने में मददगार, जैसे कि इंक्रीमेंट. जावा उसके लिए विशेष विधि कॉल की आवश्यकता होती है.) लॉक के उलट,volatile
याatomic
वैरिएबल का इस्तेमाल सीधे तौर पर नहीं किया जा सकता, ताकि अन्य थ्रेड लंबे कोड सीक्वेंस में रुकावट न डाल सकें.
यह ध्यान रखना ज़रूरी है कि volatile
का मतलब C++ और Java में है. 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 = ...
|
while (!flag) {}
|
थ्रेड 2, flag
के सेट होने का इंतज़ार कर रही है. इसलिए,
थ्रेड 2 में A
,
थ्रेड 1 में A
को असाइनमेंट असाइन किया गया. इसलिए, इन पर डेटा इकट्ठा होने की कोई संभावना नहीं है
A
. flag
को हुई दौड़ को डेटा की दौड़ के तौर पर नहीं गिना जाता,
क्योंकि, बार-बार अपडेट होने वाले डेटा या ऐटॉमिक ऐक्सेस, "सामान्य मेमोरी ऐक्सेस" नहीं होते.
मेमोरी को फिर से क्रम में लगाने से रोकने या छिपाने के लिए, लागू करना ज़रूरी है यह पिछले लिटमस टेस्ट जैसा कोड बनाने के लिए सही तरीके से काम करता है. इससे आम तौर पर, डेटा बार-बार अपडेट करने वाले या ऐटॉमिक स्टोरेज का ऐक्सेस मिलता है सामान्य ऐक्सेस से ज़्यादा महंगा है.
हालांकि पहले दिया गया उदाहरण डेटा-रेस-फ़्री है, लेकिन यह एक साथ लॉक होता है
Java में Object.wait()
या आम तौर पर C/C++ में कंडिशन वैरिएबल
आपके लिए एक बेहतर समाधान उपलब्ध कराना होगा. इसमें बार-बार इंतज़ार करने की ज़रूरत नहीं होती
बैटरी तेज़ी से खर्च हो रही है.
मेमोरी का क्रम बदलने की सूचना दिखने पर
डेटा-रेस-फ़्री प्रोग्रामिंग आम तौर पर, हमें अलग-अलग समय पर इसमें मेमोरी के ऐक्सेस को फिर से क्रम में लगाने की समस्याएं होती हैं. हालांकि, ऐसे कई मामले हैं जिनमें इससे क्रम में लगाने की प्रोसेस दिखने लगती है:- अगर आपके प्रोग्राम में कोई गड़बड़ी है, जिसकी वजह से अनजाने में डेटा इकट्ठा हो रहा है,
कंपाइलर और हार्डवेयर ट्रांसफ़ॉर्मेशन ऐक्शन को देख सकते हैं. साथ ही,
आपका प्रोग्राम हैरान हो सकता है. उदाहरण के लिए, अगर हमने पिछले उदाहरण में
flag
को volatile के तौर पर घोषित करना भूल दिया है, तो थ्रेड 2 को बिना शुरू किए गएA
दिख सकता है. या कंपाइलर यह तय कर सकता है कि फ़्लैग Thread 2 के लूप में बदला जा सकता है और प्रोग्राम कोथ्रेड 1 थ्रेड 2 A = ...
flag = truereg0 = फ़्लैग; जबकि (!reg0) {}
... = Aflag
सही है. - C++ में सुकून और शांति देने वाली सुविधाएं मिलती हैं
किसी नतीजे का क्रम में सटीक मिलान न होने के बावजूद, भले ही कोई नस्ल न हो. एटॉमिक ऑपरेशन में, साफ़ तौर पर
memory_order_
... आर्ग्युमेंट दिए जा सकते हैं. इसी तरह,java.util.concurrent.atomic
पैकेज ज़्यादा पाबंदी वाला पैकेज है एक जैसी सुविधाओं का सेट. इनमें खास तौर परlazySet()
शामिल है. साथ ही, Java प्रोग्रामर कभी-कभी इसी तरह का असर पाने के लिए, डेटा रेस का इस्तेमाल करते हैं. इन सभी तरीकों से, कैंपेन की परफ़ॉर्मेंस बेहतर होती है लागत जटिल होती है. हम उनके बारे में सिर्फ़ कुछ देर के लिए चर्चा करते हैं नीचे देखें. - कुछ C और C++ कोड किसी पुरानी स्टाइल में लिखे गए हैं, न कि पूरी तरह से
मौजूदा भाषा मानकों के साथ काम करते हैं, जिसमें
volatile
atomic
के बजाय वैरिएबल का इस्तेमाल किया जाता है और मेमोरी को क्रम में लगाया जाता है फ़ेंस डालने की अनुमति नहीं है या बाधाओं को कम करने में मदद मिलती है. इसमें ऐक्सेस के बारे में साफ़ तौर पर तर्क देना ज़रूरी है हार्डवेयर मेमोरी मॉडल को फिर से क्रम में लगाने और समझने में मदद करता है. इस तरह के कोडिंग स्टाइल का इस्तेमाल, अब भी Linux kernel में किया जाता है. इसे ऐसा नहीं करना चाहिए को नए Android ऐप्लिकेशन में इस्तेमाल किया जा सकता है और इसके बारे में यहां और चर्चा नहीं की गई है.
प्रैक्टिस करें
मेमोरी के एक जैसे होने से जुड़ी समस्याओं को डीबग करना बहुत मुश्किल हो सकता है. अगर कोई
लॉक, atomic
या volatile
एलान की वजह
पुराना डेटा पढ़ने के लिए कुछ कोड का इस्तेमाल करते हैं, तो शायद आप ये काम न कर पाएं
डीबगर की मदद से मेमोरी डंप की जांच करके इसकी वजह जानें. जितना हो सके, उतना समय
डीबगर क्वेरी जारी करते हैं, तो CPU कोर शायद
साथ ही, मेमोरी का कॉन्टेंट और सीपीयू रजिस्टर
“नामुमकिन” स्थिति.
C में क्या नहीं करें
यहां हम गलत कोड के कुछ उदाहरण तथा ऐसे आसान तरीके बताते हैं समस्या हल करें. ऐसा करने से पहले, हमें बुनियादी भाषा के इस्तेमाल के बारे में चर्चा करनी होगी सुविधा.
C/C++ और "वोलेटाइल"
C और C++ volatile
एलान, खास मकसद के लिए इस्तेमाल होने वाला टूल है.
वे कंपाइलर को वोलेटाइल को फिर से क्रम में लगाने या हटाने से रोकते हैं
ऐक्सेस करता है. यह हार्डवेयर डिवाइस रजिस्टर को ऐक्सेस करने वाले कोड के लिए मददगार हो सकता है,
मेमोरी एक से ज़्यादा स्थान पर या के साथ मैप की गई हो
setjmp
. हालांकि, Javavolatile
के मुकाबले C और C++ volatile
को थ्रेड कम्यूनिकेशन के लिए डिज़ाइन नहीं किया गया है.
C और C++ में, volatile
डेटा के ऐक्सेस को, नॉन-वोलिटाइल डेटा के ऐक्सेस के साथ फिर से व्यवस्थित किया जा सकता है. साथ ही, ऐटोमिकिटी की कोई गारंटी नहीं है. इसलिए, इनके बीच डेटा शेयर करने के लिए volatile
का इस्तेमाल नहीं किया जा सकता
यूनिप्रोसेसर पर भी, पोर्टेबल कोड में थ्रेड होते हैं. आम तौर पर, C volatile
, हार्डवेयर के ऐक्सेस को फिर से व्यवस्थित करने से नहीं रोकता. इसलिए, यह कई थ्रेड वाले एसएमपी एनवायरमेंट में भी कम काम का है. यही वजह है कि C11 और C++11 फ़ॉर्मैट काम करते हैं
atomic
ऑब्जेक्ट. इसके बजाय, आपको उनका इस्तेमाल करना चाहिए.
कई पुराने C और C++ कोड, अब भी थ्रेड के लिए volatile
का गलत इस्तेमाल करते हैं
बातचीत करते हैं. यह अक्सर उस डेटा के लिए सही तरीके से काम करता है जिसे
में इस्तेमाल किया जाता है, बशर्ते उसका इस्तेमाल साफ़ तौर पर बाड़ लगाने के साथ या किसी मामले में किया गया हो
जिसमें मेमोरी का क्रम ज़रूरी नहीं है. हालांकि, आने वाले समय में आने वाले कंपाइलर के साथ, इसके ठीक से काम करने की गारंटी नहीं है.
उदाहरण
ज़्यादातर मामलों में, लॉक लगा देना बेहतर होगा (जैसे,
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, थ्रेड 2 के दौरान initGlobalThing()
को कॉल करती है
useGlobalThing()
, gGlobalThing
को कॉल किए जा सकते हैं
लिखे जाने के दौरान ही पढ़ा जा सकता है.
इस समस्या को ठीक करने के लिए, gGlobalThing
को यह एलान करें:
ऐटमिक. C++11 में:
atomic<MyThing*> gGlobalThing(NULL);
इससे यह पक्का होता है कि लिखे गए शब्द, दूसरे थ्रेड में भी देखे जा सकेंगे
सही क्रम में. इससे कुछ अन्य गड़बड़ियों को रोकने की गारंटी भी मिलती है
ऐसे मोड जिनका इस्तेमाल करने की अनुमति फिर भी दी गई हो, लेकिन असल में इनके होने की संभावना कम हो
Android हार्डवेयर. उदाहरण के लिए, इससे यह पक्का होता है कि हम ऐसा gGlobalThing
पॉइंटर न देखें जो सिर्फ़ कुछ हिस्से में लिखा गया हो.
Java में क्या नहीं करना चाहिए
हमने Java की कुछ ज़रूरी सुविधाओं के बारे में नहीं बताया है. इसलिए, हम पहले उन पर नज़र डालें.
तकनीकी रूप से Java को, डेटा-रेस-फ़्री होने के लिए कोड की ज़रूरत नहीं है. साथ ही, इसमें बहुत सावधानी से लिखा गया कुछ Java कोड भी है, जो डेटा रेस की मौजूदगी में सही तरीके से काम करता है. हालांकि, ऐसा कोड लिखना बहुत मुश्किल होता है पेचीदा. हम इसके बारे में नीचे कम शब्दों में बताते हैं. मामले बनाने में और भी खराब, ऐसे कोड का अर्थ बताने वाले विशेषज्ञ अब ब्यौरा सही है. (डेटा-रेस-फ़्री के लिए स्पेसिफ़िकेशन ठीक है कोड.)
फ़िलहाल, हम डेटा-रेस-फ़्री मॉडल का पालन करेंगे, जिसके लिए Java
गारंटी एक जैसी है, जो C और C++ जैसी ही है. एक बार फिर, यह भाषा हमें
कुछ प्रिमिटिव हैं जो खास तौर पर क्रम में लगने वाले कंसिस्टेंसी को कम करते हैं,
lazySet()
और weakCompareAndSet()
कॉल
java.util.concurrent.atomic
में.
C और C++ की तरह ही, फ़िलहाल हम इन पर ध्यान नहीं देंगे.
Java का "सिंक किया गया" और "अनियमित" कीवर्ड
“सिंक किया गया” कीवर्ड, Java लैंग्वेज की पहले से मौजूद लॉकिंग सुविधा देता है मैकेनिज़्म. हर ऑब्जेक्ट से जुड़ा एक “मॉनिटर” होता है. इसका इस्तेमाल, एक-दूसरे से अलग ऐक्सेस देने के लिए किया जा सकता है. अगर दो थ्रेड "सिंक करने" की कोशिश करते हैं पूरी तरह कैसे उसी ऑब्जेक्ट को एक्सपोर्ट करता है, तो उनमें से एक तब तक इंतज़ार करेगा, जब तक कि दूसरा पूरा नहीं हो जाता.
जैसा कि हमने ऊपर बताया है, Java का volatile T
C++11 का atomic<T>
. एक ही समय पर इतने ऐक्सेस किए जा सकते हैं
volatile
फ़ील्ड की अनुमति है और इनकी वजह से डेटा के साथ काम नहीं होता.
lazySet()
और अन्य को अनदेखा किया जा रहा है. और डेटा रेस होती हैं, तो यह Java वीएम का काम है,
सुनिश्चित करें कि परिणाम अब भी क्रमिक रूप से एक जैसा दिखाई देता है.
खास तौर पर, अगर थ्रेड 1 किसी volatile
फ़ील्ड में लिखता है और इसके बाद, थ्रेड 2 उसी फ़ील्ड से पढ़ता है और लिखी गई नई वैल्यू देखता है, तो थ्रेड 2 को थ्रेड 1 की ओर से पहले की गई सभी लिखाई भी दिखेगी. मेमोरी इफ़ेक्ट के मामले में,
बार-बार अपडेट होने वाला डेटा, मॉनिटर रिलीज़ की तरह है और
बार-बार अपडेट होना, मॉनिटर करने वाले उपयोगकर्ता की तरह होता है.
C++ के atomic
से एक अहम अंतर है:
अगर हम Java में volatile int x;
लिखते हैं, तो x++
वैसा ही होता है जैसा x = x + 1
; यह एक बार में एक ही वैल्यू लोड करता है, नतीजे को बढ़ाता है, और फिर एक बार में एक ही वैल्यू स्टोर करता है. C++ के उलट, पूरे मामले में यह बढ़ोतरी ऐटमिक नहीं है.
इसके बजाय, ऐटॉमिक बढ़ोतरी वाली कार्रवाइयां इनके ज़रिए दी जाती हैं
java.util.concurrent.atomic
.
उदाहरण
यहां मोनोटोनिक काउंटर को आसानी से और गलत तरीके से लागू करने के बारे में बताया गया है: (Java थ्योरी और प्रैक्टिस: उतार-चढ़ाव मैनेज करना).
class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } }
मान लें कि get()
और incr()
को एक से ज़्यादा फ़ील्ड से कॉल किया गया है
है. साथ ही, हम यह पक्का करना चाहते हैं कि जब भी हर थ्रेड, मौजूदा संख्या को देखे,
get()
पर कॉल किया गया है. सबसे बड़ी समस्या यह है कि
mValue++
में असल में तीन ऑपरेशन होते हैं:
reg = mValue
reg = reg + 1
mValue = reg
अगर दो थ्रेड एक साथ incr()
में काम करते हैं, तो इनमें से एक
अपडेट खो सकते हैं. इंक्रीमेंट को एटॉमिक बनाने के लिए, हमें incr()
“synchronized” का एलान करना होगा.
हालांकि, खास तौर पर एसएमपी में यह अब भी काम नहीं कर रहा है. डेटा की दौड़ अभी तो जारी है,
get()
इसके साथ-साथ mValue
को भी ऐक्सेस कर सकता है
incr()
. Java के नियमों के तहत, get()
कॉल को दूसरे कोड के हिसाब से फिर से व्यवस्थित किया जा सकता है. उदाहरण के लिए, अगर हम
नतीजे एक जैसे दिखाई देते हैं, तो नतीजे अलग-अलग दिख सकते हैं.
क्योंकि get()
कॉल को हमने हार्डवेयर या
कंपाइलर. हम get()
को यह एलान करके समस्या को ठीक कर सकते हैं:
सिंक किया गया. इस बदलाव के साथ, कोड साफ़ तौर पर सही है.
माफ़ करें, हमने लॉक के विवाद की सुविधा शुरू की है.
परफ़ॉर्मेंस को प्रभावित कर सकता है. get()
को तय करने के बजाय
सिंक होने के बाद, हम mValue
को “डेटा में उतार-चढ़ाव” के तौर पर एलान कर सकते हैं. (नोट
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
को देखा जा सकता है. अगर volatile
कीवर्ड के साथ sGoodies
का एलान किया जाता है, तो क्रम से जुड़ी समस्या ठीक हो जाती है और सब कुछ उम्मीद के मुताबिक काम करता है.
ध्यान दें कि सिर्फ़ 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; } }
आइडिया यह है कि हम Helper
का एक इंस्टेंस रखना चाहते हैं
MyClass
के इंस्टेंस से जुड़ा ऑब्जेक्ट है. हमें केवल
इसलिए, हम इसे एक खास 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
खाली नहीं है, लेकिन इसके फ़ील्ड अभी सेट नहीं हैं और इस्तेमाल के लिए तैयार नहीं हैं.
गड़बड़ी के बारे में ज़्यादा जानकारी और गड़बड़ी वाले अन्य मोड के लिए, “‘दो बार सही का निशान लगाया गया’ सेक्शन देखें
आइटम को लॉक करने की सुविधा काम नहीं कर रही है” लिंक दिया गया है. इसमें ज़्यादा जानकारी के लिए या आइटम
71 ("लैज़ी इनिशलाइज़ेशन का इस्तेमाल समझदारी से करें"): Josh Bloch के प्रभावी Java,
दूसरा वर्शन..
इसे ठीक करने के दो तरीके हैं:
- आसान तरीके से काम करें और आउटर चेक को मिटा दें. इससे पक्का होता है कि हम
सिंक किए गए ब्लॉक के बाहर
helper
मान की जांच करें. 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 प्लैटफ़ॉर्म वर्शन पर बेहतर व्यवहार करने में मदद मिलती है. कृपया इनका इस्तेमाल करें
सही तरीके से. उदाहरण के लिए, याद रखें कि स्थिति वैरिएबल का इंतज़ार गलत तरीके से हो सकता है
वापस लौटाने के लिए कोई सिग्नल नहीं दिया जाता. इसलिए, यह एक लूप में दिखना चाहिए.
बेहतर होगा कि आप सीधे तौर पर एटॉमिक फ़ंक्शन का इस्तेमाल न करें, बशर्ते आपके पास इसे लागू करना बेहद आसान है. जैसे, कोई काउंटर. लॉक किया जा रहा है और पीथ्रेड म्यूटेक्स को अनलॉक करने के लिए, हर एक पर एक ही ऐटमिक ऑपरेशन की ज़रूरत होती है, और अक्सर एक कैश मिस होने पर भी कम लागत आती है, अगर कोई विवाद इसलिए है, ताकि आप म्यूटेक्स कॉल को एटॉमिक ऑपरेशन गैर-मामूली डेटा संरचनाओं के लिए लॉक-फ़्री डिज़ाइन की ज़रूरत होती है ज़्यादा सावधानी बरतने की ज़रूरत है, ताकि डेटा स्ट्रक्चर में ऊंचे लेवल की कार्रवाइयां की जा सकें पूरी तरह से ऐटमिक दिखता है (न कि सिर्फ़ उनके अणुओं के टुकड़े).
अगर आपने ऐटॉमिक ऑपरेशन इस्तेमाल किए हैं, तो
memory_order
... या lazySet()
से बेहतर परफ़ॉर्मेंस मिल सकती है
का लाभ उठाने के लिए, लेकिन अभी तक हमने जो बताया है उससे ज़्यादा जानकारी की ज़रूरत है.
इनका इस्तेमाल करने वाले मौजूदा कोड के ज़्यादातर हिस्से में, बाद में गड़बड़ियां पाई जाती हैं. अगर हो सके, तो इनसे बचें.
अगर आपके इस्तेमाल के उदाहरण अगले सेक्शन में दिए गए उदाहरणों में से किसी एक के हिसाब से नहीं हैं,
पक्का करें कि आप विशेषज्ञ हैं या आपने किसी विशेषज्ञ से सलाह ली है.
C/C++ में थ्रेड कम्यूनिकेशन के लिए volatile
का इस्तेमाल करने से बचें.
Java में, एक साथ कई काम करने से जुड़ी समस्याओं को अक्सर सबसे अच्छी तरह से हल किया जाता है. इसके लिए, java.util.concurrent
पैकेज की सही यूटिलिटी क्लास का इस्तेमाल किया जाता है. कोड सही तरीके से लिखा गया है
SMP पर टेस्ट किया गया.
सबसे सुरक्षित तरीका यह है कि आप चीज़ों को नहीं बदले जा सकने वाले बनाएं. ऑब्जेक्ट जावा की स्ट्रिंग और पूर्णांक धारण डेटा जैसी क्लास से ऑब्जेक्ट बनाया जाता है, जिसमें उन ऑब्जेक्ट पर डेटा रेस की सभी संभावना शामिल नहीं होती है. किताब प्रभावी Java, दूसरा वर्शन के “आइटम 15: म्यूटेबिलिटी को कम से कम करें” में खास निर्देश दिए गए हैं. इसमें नोट खास तौर पर Java फ़ील्ड को “फ़ाइनल" बताने की अहमियत (ब्लोच).
भले ही, किसी ऑब्जेक्ट में बदलाव न किया जा सकता हो, लेकिन ध्यान रखें कि किसी भी तरह के सिंक किए बिना, उसे किसी दूसरी थ्रेड से कम्यूनिकेट करना डेटा रेस है. कभी-कभी Java में ऐसा करना ठीक हो सकता है (नीचे देखें). हालांकि, इसके लिए ज़्यादा सावधानी बरतने की ज़रूरत होती है. ऐसा करने पर, कोड में गड़बड़ियां हो सकती हैं. अगर यह परफ़ॉर्मेंस के लिहाज़ से बहुत ज़रूरी नहीं है, तो volatile
एलान करें. C++ में, पॉइंटर शेयर करना या
के संदर्भ में नहीं बदला जा सकता.
डेटा की रेस की तरह, एक बग होता है.
इस स्थिति में, इसकी वजह से रुक-रुककर क्रैश होने की संभावना होती है, क्योंकि
उदाहरण के लिए, मैसेज पाने वाले थ्रेड को ऐसे तरीके की टेबल दिख सकती है जिसे शुरू न किया गया हो
पॉइंटर के रूप में नहीं बदला जा सकता.
अगर कोई मौजूदा लाइब्रेरी क्लास या नहीं बदली जा सकने वाली क्लास, दोनों नहीं है
ठीक है, Java synchronized
स्टेटमेंट या C++
सुरक्षा के लिए, lock_guard
/ unique_lock
का इस्तेमाल किया जाना चाहिए
किसी भी फ़ील्ड को ऐक्सेस करता है जिसे एक से ज़्यादा थ्रेड से ऐक्सेस किया जा सकता है. अगर आपके मामले में म्यूटेक्स काम नहीं करेंगे, तो आपको शेयर किए गए फ़ील्ड volatile
या atomic
का एलान करना चाहिए. हालांकि, आपको थ्रेड के बीच के इंटरैक्शन को समझने के लिए बहुत सावधानी बरतनी होगी. ये एलान लागू नहीं होंगे
आपको एक साथ प्रोग्राम में होने वाली आम गलतियों से बचाने में मदद मिलेगी. हालांकि, इससे आपको
कंपाइलर और एसएमपी को ऑप्टिमाइज़ करने से जुड़ी रहस्यमयी गड़बड़ियों से बचें
दुर्घटनाएं.
आपको ऐसा नहीं करना चाहिए "पब्लिश हो रहा है" किसी ऑब्जेक्ट के लिए रेफ़रंस, जैसे कि उसे दूसरे लोगों को उपलब्ध कराना थ्रेड को अपने कंस्ट्रक्टर में शामिल करें. C++ में यह कम ज़रूरी है या हमारी "कोई डेटा रेस नहीं" सलाह पाएं. लेकिन यह हमेशा एक अच्छी सलाह होती है, और ज़रूरी है, अगर आपका 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++ पर लागू होते हैं.
रेसिंग के अलावा अन्य ऐक्सेस
वैरिएबल आम तौर पर एटॉमिक होता है, क्योंकि कभी-कभी ऐसा नहीं होता
लिखने के साथ-साथ पढ़ने के लिए उपलब्ध है. हालांकि, यह समस्या सभी ऐक्सेस में नहीं आती.
उदाहरण के लिए, वैरिएबल
परमाणु हो सकता है, क्योंकि इसे एक महत्वपूर्ण सेक्शन के बाहर पढ़ा जाता है, लेकिन सभी
अपडेट को लॉक करके सुरक्षित रखा जाता है. इस स्थिति में, कोई रीड
एक ही लॉक से सुरक्षित
रेस नहीं की जा सकती, क्योंकि एक साथ लिखने की सुविधा नहीं हो सकती. ऐसी स्थिति में,
नॉन-रेसिंग ऐक्सेस (इस मामले में लोड करें), के साथ एनोटेट किया जा सकता है
C++ कोड की शुद्धता को बदले बिना memory_order_relaxed
.
लॉक लागू करने पर, ज़रूरी मेमोरी को क्रम में लगाने की सुविधा पहले से ही लागू होती है
जहां दूसरे थ्रेड से ऐक्सेस किया गया हो, और 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++ इस बात की गारंटी देता है कि ऐसे काउंटर का दूसरा लोड उसी थ्रेड में पहले के लोड से कम वैल्यू दिखाता है. जब तक काउंटर ओवरफ़्लो हो गया.) - अनुमानित संख्या का पता लगाने की कोशिश करने वाला कोड मिलना आम बात है पढ़ने और लिखने के अलग-अलग एटॉमिक (या नहीं) प्रदर्शन करके मानों की गणना करना चाहते हैं, लेकिन इससे रेवेन्यू में बढ़ोतरी नहीं हो रही है. सामान्य तर्क यह है कि यह "काफ़ी हद तक" है परफ़ॉर्मेंस काउंटर या इस तरह की अन्य चीज़ों के लिए. आम तौर पर, ऐसा नहीं होता है. जब अपडेट काफ़ी बार होते हैं (उदाहरण के लिए जो आपके लिए अहम हैं), उनकी संख्या का एक बड़ा हिस्सा आम तौर पर खो गया. क्वाड कोर डिवाइस पर, आम तौर पर आधे से ज़्यादा आंकड़े गायब हो सकते हैं. (आसान व्यायाम: दो थ्रेड की स्थिति बनाएं जिसमें काउंटर 10 लाख बार अपडेट किया गया है, लेकिन आखिरी काउंटर वैल्यू एक है.)
सिंपल फ़्लैग कम्यूनिकेशन
कोई 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<mutex> lg(mtx); if (helper == nullptr) { helper = new Helper(); } return helper; }
यह तरीका तेज़ी से लोड होने की सुविधा देता है, लेकिन डिफ़ॉल्ट रूप से क्रम के मुताबिक लगातार होने वाला, नॉन-परफ़ॉर्मेंस-क्रिटिकल स्लो पर ऑपरेशन पाथ.
यहां भी, helper.load(memory_order_acquire)
मौजूदा Android पर काम करने वाले वर्शन पर समान कोड जनरेट करने की संभावना है
आर्किटेक्चर
helper
. हमारे लिए सबसे फ़ायदेमंद ऑप्टिमाइज़ेशन होना चाहिए
पहली बार myHelper
में शुरुआत की जा सकती है
दूसरी लोडिंग भी होती है, हालांकि भविष्य का कोई कंपाइलर अपने आप ऐसा कर सकता है.
ऑर्डर लेने/रिलीज़ करने के ऑर्डर से स्टोर, लोगों को आपका स्टोर देखने से नहीं रोका जा सकता
देरी से डिलीवर किया गया हो. साथ ही, इससे यह पक्का नहीं होता कि स्टोर, दूसरे थ्रेड को दिखें
एक क्रम में. इस वजह से, यह पेचीदा,
हालांकि, यह काफ़ी सामान्य कोडिंग पैटर्न है, जिसे डेकर के आपसी बहिष्करण का उदाहरण दिया गया है.
एल्गोरिदम: सभी थ्रेड पहले एक फ़्लैग सेट करते हैं, जो बताता है कि वे करना चाहते हैं
कुछ; अगर कोई थ्रेड t है, तो देखें कि कोई दूसरी थ्रेड
कोई काम करने की कोशिश करते हुए, वह सुरक्षित रूप से आगे बढ़ सकता है.
रुकावट नहीं होगी. कोई और थ्रेड नहीं जोड़ा जाएगा
आगे नहीं बढ़ा जा सकता, क्योंकि t का फ़्लैग अब भी सेट है. अगर फ़्लैग को हासिल करने/रिलीज़ करने के क्रम का इस्तेमाल करके ऐक्सेस किया जाता है, तो यह काम नहीं करता. ऐसा इसलिए, क्योंकि इससे किसी थ्रेड का फ़्लैग, अन्य लोगों को तब भी दिख सकता है, जब वे गलती से आगे बढ़ चुके हों. डिफ़ॉल्ट memory_order_seq_cst
उसे रोकता है.
नहीं बदले जा सकने वाले फ़ील्ड
अगर पहली बार इस्तेमाल करने पर कोई ऑब्जेक्ट फ़ील्ड शुरू होता है और फिर कभी नहीं बदला जाता है, तो
शुरू किया जा सकता है और बाद में कमज़ोर तरीके से इस्तेमाल करके इसे पढ़ा जा सकता है
ऐक्सेस किया जा सकता है. C++ में, इसका एलान atomic
के तौर पर किया जा सकता है
और memory_order_relaxed
या Java का इस्तेमाल करके, इसे ऐक्सेस किया जा सकता है
volatile
के बिना एलान किया जा सकता है और बिना
खास उपायों को अपनाना होगा. इसके लिए इन सभी शर्तों को पूरा करना ज़रूरी है:
- फ़ील्ड की वैल्यू से ही बता पाना मुमकिन होना चाहिए यह पहले ही शुरू हो चुका है. फ़ील्ड को ऐक्सेस करने के लिए, फ़ास्ट पाथ की जांच और रिटर्न वैल्यू को फ़ील्ड को सिर्फ़ एक बार पढ़ना चाहिए. Java में, बाद वाली वैल्यू का होना ज़रूरी है. भले ही, फ़ील्ड को शुरू करने के तौर पर जांचा गया हो, फिर भी दूसरा लोड, पहले से शुरू नहीं की गई वैल्यू को पढ़ सकता है. C++ में "एक बार पढ़ें" नियम बस एक अच्छा तरीका नहीं है.
- इनीशियलाइज़ेशन और बाद के लोड, दोनों ऐटॉमिक होने चाहिए,
अपडेट नहीं दिखना चाहिए. Java के लिए, फ़ील्ड
long
याdouble
नहीं होना चाहिए. C++ के लिए, एटॉमिक असाइनमेंट ज़रूरी है; उसे अपनी जगह पर बनाना काम नहीं करेगा, क्योंकिatomic
की बनावट, ऐटमिक नहीं है. - बार-बार शुरू करना सुरक्षित होना चाहिए, क्योंकि कई थ्रेड में ऐसा होता है शुरू नहीं की गई वैल्यू को एक साथ पढ़ सकता है. C++ में, आम तौर पर यह "मामूली रूप से कॉपी किए जा सकने वाले" शब्द से लिया गया है सभी के लिए लागू की गई शर्त ऐटॉमिक टाइप; नेस्ट किए गए मालिकाना हक वाले पॉइंटर वाले टाइप को डील की जगह कॉपी करने की ज़रूरत नहीं है और न ही इसे कॉपी करने योग्य बनाया जा सकता है. Java के लिए, कुछ खास तरह के रेफ़रंस स्वीकार किए जाते हैं:
- Java संदर्भ, ऐसे बदले जा सकने वाले टाइप तक सीमित हैं जिनमें सिर्फ़ फ़ाइनल शामिल है फ़ील्ड. नहीं बदले जा सकने वाले कंस्ट्रक्टर को पब्लिश नहीं करना चाहिए ऑब्जेक्ट का रेफ़रंस होता है. इस मामले में, Java के फ़ाइनल फ़ील्ड के नियम पक्का करें कि अगर कोई पाठक रेफ़रंस को देखता है, तो वह शुरू किए गए फ़ाइनल फ़ील्ड. C++ में इन नियमों का कोई एनालॉग नहीं है और मालिकाना हक वाले ऑब्जेक्ट के पॉइंटर इस वजह से भी स्वीकार नहीं किए जाएंगे (इसमें साथ ही, उन समाचार संगठनों को शर्तें).
आखिरी बातें
यह दस्तावेज़ सिर्फ़ सतह को स्क्रैच नहीं करता, बल्कि ज़्यादा मैनेज नहीं किया जा सकता. यह एक बहुत बड़ा और गंभीर विषय है. कुछ सूचनाएं मिल रही हैं इन विषयों से जुड़ा जा सकता है:
- Java और C++ के असल मेमोरी मॉडल को इससे पहले होता है संबंध के हिसाब से दिखाया जाता है. इससे यह पता चलता है कि दो कार्रवाइयां किसी खास क्रम में कब होती हैं. जब हम डेटा की रेस के बारे में बताते हैं, तो हम अनौपचारिक रूप से
ने "एक साथ" होने वाले दो मेमोरी ऐक्सेस के बारे में बात की.
आधिकारिक तौर पर, इसका मतलब यह है कि कोई भी ऐसी घटना एक-दूसरे से पहले नहीं हो रही है.
इससे पहले की वास्तविक परिभाषाएं जानना एक निर्देश देता है
और Java या C++ मेमोरी मॉडल में के साथ सिंक करता है.
हालांकि, इसका मतलब है कि "एक साथ" आम तौर पर अच्छा रहता है
ये परिभाषाएं निर्देश देने वाली होती हैं. खास तौर पर तब, जब
C++ में कमज़ोर क्रम वाले ऐटॉमिक ऑपरेशन इस्तेमाल करने पर विचार कर रहे हैं.
(मौजूदा Java स्पेसिफ़िकेशन सिर्फ़
lazySet()
के बारे में बताता है अनौपचारिक रूप से.) - यह एक्सप्लोर करें कि कोड को फिर से ऑर्डर करते समय, कंपाइलर क्या कर सकते हैं और क्या नहीं. (जेएसआर-133 की खास बातों में, कानूनी बदलावों के कुछ बेहतरीन उदाहरण मौजूद हैं. इन बदलावों की वजह से अनचाहे नतीजे.)
- Java और C++ में, नहीं बदले जा सकने वाले क्लास लिखने का तरीका जानें. (इसमें और भी बहुत कुछ है न कि सिर्फ़ “बनाने के बाद कुछ भी न बदलें”.
- Effective Java, 2nd Edition के 'एक साथ कई काम करने की सुविधा' सेक्शन में दिए गए सुझावों को अपनाएं. (उदाहरण के लिए, आपको कॉल करने के ऐसे तरीके नहीं देने चाहिए जो जिसे किसी सिंक किए गए ब्लॉक में ओवरराइड किया जाना हो.)
- उपलब्ध सुविधाओं के बारे में जानने के लिए,
java.util.concurrent
औरjava.util.concurrent.atomic
एपीआई को पढ़ें. इसलिए, एक साथ कई एनोटेशन दिखाने की सुविधा, जैसे कि@ThreadSafe
और@GuardedBy
(net.jcip.annotations से).
ऐपेंडिक्स में मौजूद ज़्यादा पढ़ने के लिए सेक्शन में, उन दस्तावेज़ों और वेबसाइटों के लिंक दिए गए हैं जिनसे इन विषयों के बारे में ज़्यादा जानकारी मिलेगी.
अन्य जानकारी
सिंक्रोनाइज़ेशन स्टोर लागू करना
(ज़्यादातर प्रोग्रामर, इसे लागू नहीं करेंगे, लेकिन इस बारे में चर्चा करना ज़रूरी है.)
छोटे बिल्ट-इन टाइप, जैसे कि int
और हार्डवेयर के लिए
Android, साधारण लोड और स्टोर निर्देशों से यह पक्का होता है कि
या तो पूरी तरह से या बिलकुल भी नहीं
प्रोसेसर उसी जगह को लोड कर रहा है. इसलिए, "एटमिटी" के कुछ बुनियादी सिद्धांत मुफ़्त में उपलब्ध कराए जाते हैं.
जैसा कि हमने पहले देखा, यह तरीका काफ़ी नहीं है. यह पक्का करने के लिए कि सभी कीवर्ड हमें कार्रवाइयों का क्रम बदलने से रोकना होगा. साथ ही, यह पक्का करना होगा कि कि मेमोरी से जुड़ी कार्रवाइयां, अन्य प्रोसेस में एक जैसी ऑर्डर. ऐसा लगता है कि Android पर काम करने वाले हार्डवेयर पर, बाद वाला तरीका अपने-आप लागू हो जाता है. हालांकि, इसके लिए ज़रूरी है कि हम पहले तरीके को लागू करने के लिए सही विकल्प चुनें. इसलिए, हम यहां इस तरीके को अनदेखा करते हैं.
मेमोरी की कार्रवाइयों का ऑर्डर, दोनों को फिर से क्रम में आने से रोकता है कंपाइलर की मदद से ऐसा करता है और हार्डवेयर से दोबारा ऑर्डर करने से रोकता है. हम यहां फ़ोकस करते हैं .
ARMv7, x86, और MIPS पर मेमोरी को क्रम में लगाने की सुविधा
"बाड़" के लिए निर्देश देता है
बाड़ के दिशा-निर्देशों का पालन करने वाले निर्देशों को दिखने से रोकें
बाड़ से पहले के निर्देशों से पहले. (ये आम तौर पर,
जिसे "बैरियर" कहते हैं लेकिन ऐसा करने से उपयोगकर्ता की
pthread_barrier
-स्टाइल वाली रुकावटें, जो बहुत कुछ करती हैं
से अलग.) CANNOT TRANSLATE
बाड़ों के लिए निर्देश एक बहुत ही जटिल विषय है, जिसे ठीक करना एक मुश्किल काम है
इस तरह से, अलग-अलग तरह के बाड़ लगाने की गारंटी दी जाती है
कैसे इंटरैक्ट करते हैं और ये आम तौर पर, ऑर्डर करने की अन्य गारंटी के साथ कैसे जुड़ते हैं
हार्डवेयर उपलब्ध कराया जाता है. यह काफ़ी अहम जानकारी है. इसलिए, हम
इन चीज़ों को अच्छी तरह से समझ लें.
ऑर्डर की सबसे बुनियादी गारंटी, C++ के memory_order_acquire
और memory_order_release
ऐटॉमिक ऑपरेशन से मिलती है: रिलीज़ स्टोर से पहले के मेमोरी ऑपरेशन, ऐक्वियर लोड के बाद दिखने चाहिए. ARMv7 पर, यह
लागू करने वाला:
- बाड़ लगाने के सही निर्देश के साथ स्टोर निर्देश से पहले. इससे, स्टोर निर्देश की मदद से मेमोरी के सभी पिछले ऐक्सेस को फिर से व्यवस्थित होने से रोका जाता है. (यह अनावश्यक रूप से में स्टोर निर्देश.)
- बाड़ लगाने के सही निर्देश के साथ, लोड करने के निर्देश का पालन करना, इससे लोड को अगली ऐक्सेस के साथ फिर से क्रम में लगाए जाने से रोका जा सकता है. (और कम से कम पिछले लोड के साथ, फिर से ग़ैर-ज़रूरी क्रम देना.)
यह C++ इस्तेमाल करने/रिलीज़ ऑर्डर करने के लिए काफ़ी है.
ये Java volatile
के लिए ज़रूरी हैं, लेकिन काफ़ी नहीं हैं
या C++ क्रम से एक जैसा atomic
.
यह जानने के लिए कि हमें और किन चीज़ों की ज़रूरत है, Dekker के एल्गोरिदम के हिस्से पर गौर करें
जिसके बारे में हम पहले ही बता चुके हैं.
flag1
और flag2
, C++ atomic
हैं
या Java volatile
वैरिएबल, दोनों शुरुआत में गलत हैं.
थ्रेड 1 | थ्रेड 2 |
---|---|
flag1 = true |
flag2 = true |
क्रम के मुताबिक एक जैसे होने का मतलब है कि एक असाइनमेंट में
flag
n को सबसे पहले एक्ज़ीक्यूट करना चाहिए और इसे
को दूसरे थ्रेड में टेस्ट करें. इसलिए, हम
ये थ्रेड एक साथ "अहम-सामान" को एक्ज़ीक्यूट करती हैं.
हालांकि, रिलीज़-रिलीज़ के क्रम के लिए ज़रूरी तलवारबाज़ी
हर थ्रेड की शुरुआत और आखिर में बाड़ होनी चाहिए. इससे कोई मदद नहीं मिलती
यहां. साथ ही, हमें यह पक्का करना होगा कि अगर
atomic
में से volatile
स्टोर के बाद, आता है
volatile
/atomic
लोड होते हैं, तो दोनों का क्रम नहीं बदला जाता.
आम तौर पर किसी फ़ेंस को जोड़ने से
यह एक ही स्टोर में स्टोर करता है. यह बाद में भी इस्तेमाल होता है.
(यह फिर से ज़रूरत से ज़्यादा मज़बूत है, क्योंकि आम तौर पर यह बाड़ ऑर्डर करती है
बाद की सभी मेमोरी ऐक्सेस करने वाली मेमोरी को ऐक्सेस नहीं किया जा सकेगा.)
इसके बजाय, हम अतिरिक्त बाड़ को क्रम से एक जैसा लोड होता है. दुकानों की संख्या कम होती है, इसलिए यह तरीका अपनाया जाता है आम तौर पर, Android डिवाइसों के लिए इसका इस्तेमाल किया जाता है.
जैसा कि हमने पिछले सेक्शन में देखा था, हमें स्टोर/लोड बैरियर डालने की ज़रूरत होती है पर नज़र रखें. बार-बार अपडेट होने वाले ऐक्सेस के लिए, वीएम में एक्ज़ीक्यूट किया गया कोड कुछ ऐसा दिखेगा:
बार-बार होने वाला लोड | लगातार अपडेट होने वाला स्टोर |
---|---|
reg = A |
fence for "release" (2) |
असल मशीन आर्किटेक्चर में आम तौर पर कई तरह के फ़ेंस होते हैं. ये अलग-अलग तरह के ऐक्सेस का ऑर्डर देते हैं और इनकी कीमत अलग-अलग हो सकती है. इनमें से किसी एक को चुनना आसान नहीं है. यह इस बात पर निर्भर करता है कि स्टोर को अन्य कोर को एक जैसे क्रम में दिखाया जाए या नहीं. साथ ही, यह भी ज़रूरी है कि कई फ़ेंस के कॉम्बिनेशन से लागू की गई मेमोरी का क्रम सही तरीके से कॉम्पोज़ हो. ज़्यादा जानकारी के लिए, कृपया के साथ कैम्ब्रिज यूनिवर्सिटी का पेज देखें असली प्रोसेसर के लिए एटॉमिक्स की इकट्ठा की गई मैपिंग.
कुछ आर्किटेक्चर पर, खास तौर पर x86, "उपयोगकर्ता हासिल करना" और "रिलीज़" हो सकता है बाधाएं अनावश्यक होती हैं, क्योंकि हार्डवेयर हमेशा अस्पष्ट रूप से काफ़ी ऑर्डरिंग लागू करता है. इस तरह, x86 पर सिर्फ़ आखिरी बाड़ (3) जनरेट होती है. इसी तरह, x86 पर, ऐटॉमिक पढ़ें-बदलाव करें-लिखें कार्रवाइयों का मतलब मज़बूत बाड़ है. इसलिए, इनके लिए कभी भी फ़ेंस की ज़रूरत नहीं होती. हमने ARMv7 पर, ऊपर बताए गए सभी बाड़ों पर आवश्यक.
ARMv8, LDAR और STLR के लिए निर्देश देता है, जो यह नीति, Java में लगातार अपडेट होने वाले या C++ और लगातार एक जैसे होने पर लागू होने वाली शर्तों को लागू करती है लोड और स्टोर. इनसे, क्रम में बदलाव करने से जुड़ी उन ग़ैर-ज़रूरी पाबंदियों से बचा जा सकता है जिनके बारे में हमने ऊपर बताया है. ARM पर मौजूद 64-बिट Android कोड इनका इस्तेमाल करता है; हमने चुना हम ARMv7 बाड़ लगाने की जगह पर फ़ोकस करना चाहते हैं, क्योंकि यह सुरक्षा के लिए सबसे ज़्यादा सभी ज़रूरी शर्तों को पूरा करना होगा.
इसके बारे में और पढ़ें
ज़्यादा गहराई या विस्तार से जानकारी देने वाले वेब पेज और दस्तावेज़. सामान्य तौर पर ज़्यादा काम की लेख सूची में सबसे ऊपर हैं.
- शेयर की गई मेमोरी की क्षमता बनाए रखने वाले मॉडल: एक ट्यूटोरियल
- Adve और Gharachorloo ने इसे 1995 में लिखा था. अगर आपको मेमोरी के कॉन्सिस्टेन्सी मॉडल के बारे में ज़्यादा जानना है, तो यह एक अच्छी शुरुआत है.
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf - मेमोरी की रुकावटें
- समस्याओं के बारे में खास जानकारी देने वाला छोटा सा लेख.
https://en.wikipedia.org/wiki/Memory_barrier - Threads से जुड़ी बुनियादी बातें
- Hans Boehm की ओर से लिखी गई, C++ और Java में मल्टी-थ्रेड प्रोग्रामिंग के बारे में जानकारी. डेटा रेस और सिंक करने के बुनियादी तरीकों के बारे में चर्चा.
http://www.hboehm.info/c++mm/threadsintro.html - Java कॉन करंसी का इस्तेमाल
- साल 2006 में पब्लिश हुई इस किताब में, कई तरह के विषयों पर विस्तार से बात की गई है. Java में मल्टी-थ्रेड कोड लिखने वाले किसी भी व्यक्ति के लिए खास तौर पर इसका सुझाव दिया जाता है.
http://www.javaconcurrencyinpractice.com - JSR-133 (Java मेमोरी मॉडल) के बारे में अक्सर पूछे जाने वाले सवाल
- 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 थ्योरी और प्रैक्टिस: Java में सुरक्षित निर्माण की तकनीकें
- इस लेख में, ऑब्जेक्ट बनाते समय रेफ़रंस एस्केपिंग से जुड़े खतरों की जांच की गई है. साथ ही, थ्रेड को सुरक्षित करने वाले कंस्ट्रक्टर के लिए दिशा-निर्देश भी दिए गए हैं.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html - Java थ्योरी ऐंड प्रैक्टिस: मैनेजिंग वोलैटिलिटी
- एक अच्छा लेख, जिसमें यह बताया गया है कि Java में बार-बार अपडेट होने वाले फ़ील्ड की मदद से क्या-क्या हासिल किया जा सकता है और क्या नहीं.
http://www.ibm.com/developerworks/java/library/j-jtp06197.html - “दो बार सही का निशान लगाकर लॉक करने की सुविधा सही तरीके से काम नहीं कर रही है” का एलान
- बिल पुग ने उन अलग-अलग तरीकों के बारे में पूरी जानकारी दी है जिनकी मदद से, दोबारा चेक की गई लॉक करने की प्रोसेस को
volatile
याatomic
के बिना पूरा नहीं किया जा सकता. C/C++ और Java शामिल हैं.
http://www.cs.umd.edu/~pugh/java/memoryModel/ब्लूटूथ CheckedLocking.html - [ARM] बैरियर लिटमस टेस्ट और कुकबुक
- ARM SMP की समस्याओं पर चर्चा, जिसमें ARM कोड के छोटे-छोटे स्निपेट शामिल किए गए हैं. अगर आपको इस पेज पर दिए गए उदाहरणों के बारे में ज़्यादा जानकारी नहीं है या आपको डीएमबी निर्देश का औपचारिक ब्यौरा पढ़ना है, तो इसे पढ़ें. यह एक्ज़ीक्यूटेबल कोड पर मेमोरी संबंधी रुकावटों के लिए इस्तेमाल किए जाने वाले निर्देशों के बारे में भी बताता है (यह तब काम आ सकता है, जब कोड को तुरंत जनरेट किया जा रहा हो). ध्यान दें कि यह ARMv8 से पहले का है, जो
मेमोरी को क्रम में लगाने के अतिरिक्त निर्देश इस्तेमाल किए जा सकते हैं और इन्हें बेहतर बनाया गया है
मेमोरी मॉडल में सेव किया जाएगा. ज़्यादा जानकारी के लिए, "ARM® आर्किटेक्चर रेफ़रंस मैन्युअल ARMv8, for ARMv8-A architecture profile" देखें.
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf - Linux कर्नेल मेमोरी बैरियर
- Linux कर्नेल मेमोरी बैरियर के लिए दस्तावेज़. इसमें कुछ काम के उदाहरण और ASCII आर्ट शामिल है.
http://www.kernel.org/doc/docs/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 (सी मानक) 9899 (C प्रोग्रामिंग भाषा) चैप्टर 7.16 (“ऐटॉमिक्स <stdatomic.h>”)
- ISO/IEC 9899-201x C एटॉमिक ऑपरेशन की सुविधाओं के लिए ड्राफ़्ट स्टैंडर्ड.
ज़्यादा जानकारी के लिए, गड़बड़ी की बाद की रिपोर्ट भी देखें.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf - प्रोसेसर के लिए C/C+11 मैपिंग (कैंब्रिज यूनिवर्सिटी)
- जारोस्लाव सेवचिक और पीटर सेवेल का अनुवाद का संग्रह
में C++ एटॉमिक्स शामिल हैं.
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html - Dekker का एल्गोरिदम
- “एक साथ काम करने वाले प्रोग्रामिंग में, म्यूचुअरी एक्सक्लूज़न की समस्या का पहला सही हल”. Wikipedia के लेख में पूरा एल्गोरिदम दिया गया है. साथ ही, इस बात की चर्चा की गई है कि मॉडर्न ऑप्टिमाइज़ेशन कंपाइलर और एसएमपी हार्डवेयर के साथ काम करने के लिए, इसे कैसे अपडेट करना होगा.
https://en.wikipedia.org/wiki/Dekker's_algorithm - ARM बनाम ऐल्फ़ा वर्शन और पता डिपेंडेंसी पर टिप्पणियां
- कैटलिन मैरीना से आर्म-कर्नेल ईमेल पाने वाले लोगों की सूची में एक ईमेल. इसमें पते की खास जानकारी और कंट्रोल डिपेंडेंसी शामिल होती हैं.
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html - हर प्रोग्रामर को मेमोरी के बारे में क्या पता होना चाहिए
- उलरिच ड्रेपर की ओर से, अलग-अलग तरह की मेमोरी, खास तौर पर सीपीयू कैश के बारे में एक बहुत बड़ा और विस्तार से लेख.
http://www.akkadia.org/drapper/cpumemory.pdf - ARM के कमज़ोर एक जैसे मेमोरी मॉडल के बारे में तर्क
- इस पेपर को चोंग और ने लिखा था ARM, Ltd. का इश्तियाक. यह ARM एसएमपी के मेमोरी मॉडल को सख्ती से, लेकिन आसानी से ऐक्सेस करने लायक बनाने की कोशिश करता है. यहां इस्तेमाल की गई “निगरानी” की परिभाषा, इस पेपर से ली गई है. एक बार फिर, यह ARMv8 से पहले का है.
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&coll\rdl लगतीCFID=96099715&CFTOKEN=57505711 - कंपाइलर राइटर्स के लिए JSR-133 कुकबुक
- डग ली ने इसे जेएसआर-133 (जावा मेमोरी मॉडल) दस्तावेज़ के साथी के तौर पर लिखा था. इसमें, लागू करने के दिशा-निर्देशों का शुरुआती सेट शामिल है
का इस्तेमाल किया जाता है, जिसका इस्तेमाल कई कंपाइलर लेखकों ने किया था. साथ ही,
अब भी बड़े पैमाने पर उद्धरण देते हैं और जिनके ज़रिए अहम जानकारी दी जा सकती है.
दुर्भाग्य से, बाड़ के जिन चार प्रकारों के बारे में यहां बताया गया है, वे अच्छे नहीं हैं
Android के साथ काम करने वाले आर्किटेक्चर और ऊपर दी गई C++11 मैपिंग के हिसाब से मैच करें
अब जावा के लिए भी, सटीक रेसिपी का बेहतर स्रोत हैं.
http://g.oswego.edu/dl/jmm/cookbook.html - x86-TSO: x86 मल्टीप्रोसेसर के लिए एक सख्त और उपयोग किया जा सकने वाला प्रोग्रामर का मॉडल
- x86 मेमोरी मॉडल की सटीक जानकारी. इसका सटीक ब्यौरा
माफ़ करें, ARM मेमोरी मॉडल काफ़ी जटिल हैं.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf