यह एक उदाहरण के साथ सबसे अच्छा सचित्र है।
मान लें कि हमारे पास एक सरल कार्य है जिसे हम समानांतर में कई बार प्रदर्शन करना चाहते हैं, और हम उस कार्य की संख्या को विश्व स्तर पर ट्रैक करना चाहते हैं, उदाहरण के लिए, एक वेब पेज पर हिट की गिनती।
जब प्रत्येक थ्रेड उस बिंदु पर पहुंच जाता है जिस पर वह गिनती बढ़ाता है, तो उसका निष्पादन इस तरह दिखाई देगा:
- प्रोसेसर रजिस्टर में मेमोरी से हिट की संख्या पढ़ें
- वह संख्या बढ़ाना।
- उस नंबर को मेमोरी में वापस लिखें
याद रखें कि हर धागा इस प्रक्रिया में किसी भी बिंदु पर निलंबित कर सकता है। इसलिए यदि थ्रेड ए चरण 1 करता है, और फिर निलंबित हो जाता है, थ्रेड बी द्वारा सभी तीन चरणों का पालन करते हुए, जब थ्रेड ए फिर से शुरू होता है, तो इसके रजिस्टरों में गलत संख्या हिट होगी: इसके रजिस्टरों को बहाल किया जाएगा, यह पुरानी संख्या को खुशी से बढ़ाएगा। हिट्स की, और उस बढ़े हुए नंबर को स्टोर करें।
इसके अलावा, किसी भी संख्या में अन्य थ्रेड उस समय के दौरान चल सकते थे जब थ्रेड A को निलंबित कर दिया गया था, इसलिए अंत में लिखने वाला थ्रेड A सही गणना के ठीक नीचे हो सकता है।
इस कारण से, यह सुनिश्चित करना आवश्यक है कि यदि कोई थ्रेड चरण 1 करता है, तो उसे स्टेप 3 करने से पहले किसी अन्य थ्रेड को चरण 1 करने की अनुमति है, जो कि इस प्रक्रिया को प्रारंभ करने से पहले एक भी लॉक प्राप्त करने की प्रतीक्षा कर रहे सभी थ्रेड द्वारा पूरा किया जा सकता है , और प्रक्रिया पूरी होने के बाद ही लॉक को मुक्त करना, ताकि कोड के इस "महत्वपूर्ण खंड" को गलत तरीके से इंटरलीव न किया जा सके, जिसके परिणामस्वरूप गलत गणना हो।
लेकिन क्या होगा अगर ऑपरेशन परमाणु थे?
हां, जादुई गेंडा और इंद्रधनुष की भूमि में, जहां वृद्धि ऑपरेशन परमाणु है, फिर उपरोक्त उदाहरण के लिए ताला लगाना आवश्यक नहीं होगा।
हालांकि, यह महसूस करना महत्वपूर्ण है कि हम जादुई गेंडा और इंद्रधनुष की दुनिया में बहुत कम समय बिताते हैं। लगभग हर प्रोग्रामिंग लैंग्वेज में इंक्रीमेंट ऑपरेशन उपरोक्त तीन चरणों में टूट जाता है। ऐसा इसलिए है, भले ही प्रोसेसर एक परमाणु वेतन वृद्धि ऑपरेशन का समर्थन करता है, यह ऑपरेशन काफी अधिक महंगा है: इसे मेमोरी से पढ़ना है, संख्या को संशोधित करना है, और इसे मेमोरी में वापस लिखना है ... और आमतौर पर परमाणु वेतन वृद्धि एक ऑपरेशन है विफल हो सकता है, जिसका अर्थ है कि ऊपर दिए गए सरल अनुक्रम को एक लूप से बदलना होगा (जैसा कि हम नीचे देखेंगे)।
चूंकि, मल्टीथ्रेडेड कोड में भी, कई वेरिएबल्स को एक थ्रेड के लिए स्थानीय रखा जाता है, अगर वे प्रत्येक वैरिएबल को एक थ्रेड के लिए स्थानीय मान लेते हैं, तो प्रोग्राम बहुत अधिक कुशल होते हैं, और प्रोग्रामर थ्रेड्स के बीच साझा स्थिति की रक्षा करने का ध्यान रखते हैं। विशेष रूप से यह देखते हुए कि परमाणु संचालन आमतौर पर थ्रेडिंग मुद्दों को हल करने के लिए पर्याप्त नहीं है, जैसा कि हम बाद में देखेंगे।
अस्थिर चर
यदि हम इस विशेष समस्या के लिए ताले से बचना चाहते हैं, तो हमें पहले यह महसूस करना होगा कि हमारे पहले उदाहरण में दर्शाए गए कदम वास्तव में आधुनिक संकलित कोड में नहीं हैं। क्योंकि कंपाइलर मानते हैं कि केवल एक धागा ही चर को संशोधित कर रहा है, प्रत्येक धागा चर की अपनी स्वयं की कैश्ड कॉपी रखेगा, जब तक कि प्रोसेसर रजिस्टर को किसी और चीज के लिए आवश्यक नहीं है। जब तक यह कैश्ड कॉपी है, यह मानता है कि इसे मेमोरी में वापस जाने और इसे फिर से पढ़ने की ज़रूरत नहीं है (जो महंगा होगा)। जब तक यह रजिस्टर में रखा जाता है तब तक वे चर को वापस मेमोरी में नहीं लिखेंगे।
हम उस स्थिति में वापस आ सकते हैं, जिसे हमने पहले उदाहरण में दिया था (ऊपर बताई गई सभी समान थ्रेडिंग समस्याओं के साथ) चर को अस्थिर के रूप में चिह्नित करके , जो संकलक को बताता है कि यह चर दूसरों द्वारा संशोधित किया जा रहा है, और इसलिए इसे अवश्य पढ़ें या जब भी इसे एक्सेस या संशोधित किया जाता है, मेमोरी को लिखा जाता है।
तो अस्थिर के रूप में चिह्नित एक चर हमें परमाणु वृद्धि कार्यों की भूमि पर नहीं ले जाएगा, यह केवल हमें उतना ही करीब ले जाता है जितना हमने सोचा था कि हम पहले से ही थे।
वृद्धि को परमाणु बनाना
एक बार जब हम एक अस्थिर चर का उपयोग कर रहे होते हैं, तो हम एक निम्न-स्तरीय सशर्त सेट ऑपरेशन का उपयोग करके हमारे वेतन वृद्धि को परमाणु बना सकते हैं जो कि अधिकांश आधुनिक सीपीयू समर्थन करते हैं (अक्सर तुलना और सेट या तुलना और स्वैप कहा जाता है )। यह दृष्टिकोण, उदाहरण के लिए, जावा के एटॉमिकइंटर क्लास में लिया गया है:
197 /**
198 * Atomically increments by one the current value.
199 *
200 * @return the updated value
201 */
202 public final int incrementAndGet() {
203 for (;;) {
204 int current = get();
205 int next = current + 1;
206 if (compareAndSet(current, next))
207 return next;
208 }
209 }
उपरोक्त लूप बार-बार निम्न चरणों का पालन करता है, जब तक कि चरण 3 सफल न हो जाए:
- मेमोरी से सीधे वाष्पशील चर का मान पढ़ें।
- उस मूल्य में वृद्धि।
- मान को (मुख्य मेमोरी में) बदलें और यदि केवल मुख्य मेमोरी में इसका वर्तमान मान वैसा ही हो जैसा कि हम शुरू में पढ़ते हैं, तो एक विशेष परमाणु ऑपरेशन का उपयोग करके।
यदि चरण 3 विफल रहता है (क्योंकि चरण 1 के बाद एक अलग थ्रेड द्वारा मूल्य बदल दिया गया था), यह फिर से मुख्य मेमोरी से सीधे चर को पढ़ता है और फिर से कोशिश करता है।
जबकि तुलना-और-स्वैप ऑपरेशन महंगा है, इस मामले में लॉकिंग का उपयोग करने की तुलना में यह थोड़ा बेहतर है, क्योंकि यदि चरण 1 के बाद एक थ्रेड निलंबित हो जाता है, तो चरण 1 तक पहुंचने वाले अन्य थ्रेड्स को ब्लॉक नहीं करना पड़ता है और पहले थ्रेड का इंतजार करना पड़ता है, जो महंगा संदर्भ स्विचिंग को रोका जा सकता है। जब पहला धागा फिर से शुरू होता है, तो यह चर लिखने के अपने पहले प्रयास में विफल हो जाएगा, लेकिन चर को फिर से पढ़ना जारी रखने में सक्षम होगा, जो फिर से लॉक होने के साथ आवश्यक संदर्भ स्विच की तुलना में कम महंगा होगा।
इसलिए, हम वास्तविक ताले का उपयोग, तुलना और स्वैप के बिना परमाणु वृद्धि (या एक ही चर पर अन्य संचालन) की भूमि पर कर सकते हैं।
तो जब लॉकिंग सख्ती से आवश्यक है?
यदि आपको एक परमाणु संचालन में एक से अधिक चर को संशोधित करने की आवश्यकता है, तो लॉकिंग आवश्यक होगा, आपको उसके लिए एक विशेष प्रोसेसर निर्देश नहीं मिलेगा।
जब तक आप एक एकल चर पर काम कर रहे हैं, और आप जो भी काम करने में विफल रहे हैं और चर को पढ़ने और फिर से शुरू करने के लिए तैयार हैं, तुलना और-अदला-बदली काफी अच्छी होगी, हालांकि।
आइए एक उदाहरण पर विचार करें जहां प्रत्येक थ्रेड पहले चर एक्स में 2 जोड़ता है, और फिर एक्स को दो से गुणा करता है।
यदि X शुरू में एक है, और दो धागे चलते हैं, तो हम परिणाम ((1 + 2) * 2) + 2) * 2 = 16 होने की उम्मीद करते हैं।
हालाँकि, यदि थ्रेड्स इंटरलेवेव करते हैं, तो हम सभी ऑपरेशनों के परमाणु होने के बावजूद भी कर सकते हैं, इसके बजाय दोनों जोड़ पहले होते हैं, और गुणा के बाद आते हैं, जिसके परिणामस्वरूप (1 + 2 + 2) * 2 * 2 = 20 होता है।
ऐसा इसलिए होता है क्योंकि गुणन और जोड़ कम्यूटेटिव ऑपरेशन नहीं होते हैं।
इसलिए, खुद के परमाणु होने के लिए ऑपरेशन पर्याप्त नहीं है, हमें ऑपरेशन के संयोजन को परमाणु बनाना चाहिए।
हम या तो प्रक्रिया को क्रमबद्ध करने के लिए लॉकिंग का उपयोग करके कर सकते हैं, या हम एक स्थानीय चर का उपयोग कर सकते हैं जब हम अपनी गणना शुरू करते हैं, तो एक्स के मूल्य को संग्रहीत करने के लिए, मध्यवर्ती चरणों के लिए एक दूसरा स्थानीय चर, और फिर तुलना-और-स्वैप का उपयोग करें। केवल एक नया मान सेट करें यदि X का वर्तमान मान X के मूल मान के समान है। यदि हम विफल होते हैं, तो हमें X को पढ़ने और गणनाओं को फिर से निष्पादित करके फिर से शुरू करना होगा।
इसमें कई ट्रेड-ऑफ शामिल हैं: जैसे-जैसे गणना लंबी होती जाती है, यह बहुत अधिक हो जाता है कि चल रहे धागे को निलंबित कर दिया जाएगा, और मूल्य फिर से शुरू होने से पहले एक और धागे द्वारा संशोधित किया जाएगा, जिसका अर्थ है कि असफलताएं बहुत अधिक होने की संभावना है, जिससे बर्बाद हो जाता है। प्रोसेसर समय। बहुत लंबे समय तक चलने वाली गणना के साथ बड़ी संख्या में थ्रेड्स के चरम मामले में, हमारे पास 100 थ्रेड्स वेरिएबल को पढ़ सकते हैं और गणनाओं में लगे हो सकते हैं, इस स्थिति में केवल पहली बार समाप्त होने पर नया मान लिखने में सफल होंगे, अन्य 99 अभी भी अपनी गणना पूरी करें, लेकिन पूरा होने पर पता चलता है कि वे मूल्य को अपडेट नहीं कर सकते हैं ... जिस बिंदु पर वे प्रत्येक मूल्य को पढ़ेंगे और गणना शुरू करेंगे। हम संभवतः शेष 99 धागे एक ही समस्या को दोहराएंगे, जिससे प्रोसेसर की बड़ी मात्रा बर्बाद हो जाएगी।
ताले के माध्यम से महत्वपूर्ण खंड का पूर्ण क्रमांकन उस स्थिति में बहुत बेहतर होगा: 99 धागे निलंबित होंगे जब उन्हें ताला नहीं मिला था, और हम लॉकिंग बिंदु पर आगमन के क्रम में प्रत्येक थ्रेड को चलाएंगे।
यदि क्रमांकन महत्वपूर्ण नहीं है (जैसा कि हमारे वेतन वृद्धि के मामले में), और गणना विफल हो जाती है यदि संख्या को अद्यतन करने में विफल रहता है तो न्यूनतम हैं, तुलनात्मक और स्वैप ऑपरेशन का उपयोग करने से प्राप्त होने वाला एक महत्वपूर्ण लाभ हो सकता है, क्योंकि वह ऑपरेशन ताला लगाने से कम खर्चीला है।