मैं लगभग 15 वर्षों से सॉफ्टवेयर इंजीनियर हूं। अपने करियर के दौरान मैंने बहुत कुछ सीखा और इन सीखों को कई वितरित प्रणालियों को डिजाइन और कार्यान्वित करने (और कभी-कभी चरणबद्ध तरीके से समाप्त करने या वैसे ही छोड़ने) में लागू किया। रास्ते में मैंने अनगिनत गलतियाँ कीं और अब भी कर रहा हूँ। लेकिन चूंकि मेरा मुख्य ध्यान विश्वसनीयता पर था, इसलिए मैं त्रुटि आवृत्ति को कम करने के तरीके खोजने के लिए अपने अनुभव और समुदाय को देख रहा हूं। मेरा आदर्श वाक्य है: हमें बिल्कुल नई गलतियाँ करने की कोशिश करनी चाहिए (कम स्पष्ट, अधिक परिष्कृत)। गलती करना ठीक है - हम इसी तरह सीखते हैं, दोहराना - दुखद और हतोत्साहित करने वाला है।
शायद यही बात मुझे हमेशा गणित के प्रति आकर्षित करती रही है। न केवल इसलिए कि यह सुरुचिपूर्ण और संक्षिप्त है, बल्कि इसलिए कि इसकी तार्किक कठोरता गलतियों को रोकती है। यह आपको अपने वर्तमान संदर्भ के बारे में सोचने के लिए मजबूर करता है कि आप किन सिद्धांतों और प्रमेयों पर भरोसा कर सकते हैं। इन नियमों का पालन करना फलदायक साबित होता है, आपको सही परिणाम मिलता है। यह सच है कि कंप्यूटर विज्ञान गणित की एक शाखा है। लेकिन आमतौर पर हम जो अभ्यास करते हैं वह सॉफ्टवेयर इंजीनियरिंग है, जो एक बहुत ही अलग चीज है। हम समय की कमी और व्यावसायिक आवश्यकताओं को ध्यान में रखते हुए कंप्यूटर विज्ञान की उपलब्धियों और खोजों को अभ्यास में लागू करते हैं। यह ब्लॉग कंप्यूटर प्रोग्रामों के डिज़ाइन और कार्यान्वयन पर अर्ध-गणितीय तर्क लागू करने का एक प्रयास है। हम विभिन्न निष्पादन व्यवस्थाओं का एक मॉडल सामने रखेंगे जो कई प्रोग्रामिंग त्रुटियों से बचने के लिए एक रूपरेखा प्रदान करता है।
जब हम प्रोग्राम करना सीखते हैं और अपना पहला अस्थायी (या साहसी) कदम उठाते हैं तो हम आम तौर पर कुछ सरल से शुरू करते हैं:
हम मांसपेशियों की स्मृति प्राप्त करते हैं, भाषा वाक्यविन्यास सीखते हैं और सबसे महत्वपूर्ण बात यह है कि हम अपने सोचने और तर्क करने के तरीके को बदलते हैं। हम कोड को पढ़ना सीखते हैं, यह अनुमान लगाते हैं कि इसे कैसे निष्पादित किया जा रहा है। हम लगभग कभी भी किसी भाषा मानक को पढ़ने से शुरुआत नहीं करते हैं और उसके "मेमोरी मॉडल" खंड को बारीकी से नहीं पढ़ते हैं - क्योंकि हम अभी तक उन्हें पूरी तरह से समझने और उनका उपयोग करने के लिए तैयार नहीं हैं। हम परीक्षण और त्रुटि का अभ्यास करते हैं: हम अपने पहले कार्यक्रमों में तार्किक और अंकगणितीय बग पेश करते हैं। ये गलतियाँ हमें अपनी धारणाओं की जाँच करना सिखाती हैं: क्या यह लूप अपरिवर्तनीय सही है, क्या हम सरणी तत्व के सूचकांक और लंबाई की तुलना इस तरह से कर सकते हैं (आप इसे -1 कहाँ रखते हैं)? लेकिन अगर हमें किसी प्रकार की त्रुटियाँ नज़र नहीं आतीं, तो कई बार हम परोक्ष रूप से उनमें से कुछ को अपने अंदर समाहित कर लेते हैं
अर्थात् यह वाला:
कोड की पंक्तियों का मूल्यांकन हमेशा एक ही क्रम में (क्रमबद्ध) किया जाता है।
यह अभिधारणा हमें यह मानने की अनुमति देती है कि अगले प्रस्ताव सत्य हैं (हम उन्हें साबित नहीं करने जा रहे हैं):
गणितीय सिद्धांत ठोस आधार पर बड़ी संरचनाओं को प्राप्त करने और बनाने की अनुमति देते हैं। गणित में, हमारे पास 4+1 अभिधारणा वाली यूक्लिडियन ज्यामिति है। आखिरी वाला कहता है:
समानांतर रेखाएँ समानांतर रहती हैं, वे प्रतिच्छेद या विमुख नहीं होती हैं
सहस्राब्दियों तक गणितज्ञों ने इसे सिद्ध करने का प्रयास किया, और इसे पहले चार से प्राप्त किया। पता चला कि यह संभव नहीं है. हम इस "समानांतर रेखाओं" को विकल्पों के साथ बदल सकते हैं और विभिन्न प्रकार की ज्यामिति (अर्थात्, अतिशयोक्तिपूर्ण और अण्डाकार) प्राप्त कर सकते हैं, जो नई संभावनाएं खोलती हैं और लागू और उपयोगी साबित होती हैं। आख़िरकार हमारे अपने ग्रह की सतह समतल नहीं है और हमें इसका हिसाब रखना होगा, उदाहरण के लिए जीपीएस सॉफ़्टवेयर और हवाई जहाज़ मार्गों में।
लेकिन उससे पहले आइए रुकें और अधिकांश इंजीनियरिंग प्रश्न पूछें: परेशान क्यों हों? यदि प्रोग्राम अपना काम करता है, इसका समर्थन करना, रखरखाव करना और विकसित करना आसान है, तो हमें पहले स्थान पर पूर्वानुमानित अनुक्रमिक निष्पादन के इस आरामदायक अपरिवर्तनीय को क्यों छोड़ना चाहिए?
मुझे दो उत्तर दिख रहे हैं. पहला है प्रदर्शन . यदि हम अपने प्रोग्राम को दोगुनी तेजी से या समान गति से चला सकते हैं - जिसके लिए आधे हार्डवेयर की आवश्यकता होती है - तो यह एक इंजीनियरिंग उपलब्धि है। यदि समान मात्रा में कम्प्यूटेशनल संसाधनों का उपयोग करके हम 2x (या 3, 4, 5, 10x) डेटा को पीस सकते हैं - तो यह उसी प्रोग्राम के पूरी तरह से नए एप्लिकेशन खोल सकता है। यह सर्वर के बजाय आपकी जेब में मौजूद मोबाइल फोन पर चल सकता है। कभी-कभी हम चतुर एल्गोरिदम लागू करके या अधिक प्रदर्शन करने वाली भाषा में फिर से लिखकर गति प्राप्त कर सकते हैं। हां, तलाशने के लिए ये हमारे पहले विकल्प हैं। लेकिन उनकी एक सीमा है. आर्किटेक्चर लगभग हमेशा कार्यान्वयन को मात देता है। मूर का नियम हाल ही में उतना अच्छा प्रदर्शन नहीं कर रहा है, एकल सीपीयू का प्रदर्शन धीरे-धीरे बढ़ रहा है, रैम प्रदर्शन (विलंबता, मुख्य रूप से) पिछड़ रहा है। इसलिए, स्वाभाविक रूप से, इंजीनियरों ने अन्य विकल्पों की तलाश शुरू कर दी।
दूसरा विचार विश्वसनीयता है. प्रकृति अव्यवस्थित है, थर्मोडायनामिक्स का दूसरा नियम लगातार किसी भी सटीक, अनुक्रमिक और दोहराए जाने योग्य चीज़ के विरुद्ध काम करता है। बिट्स फ़्लिप हो जाते हैं, सामग्री खराब हो जाती है, बिजली बंद हो जाती है, तार कट जाते हैं जिससे हमारे प्रोग्राम का निष्पादन रुक जाता है। अनुक्रमिक और दोहराव योग्य अमूर्तता बनाए रखना एक कठिन काम बन जाता है। यदि हमारे प्रोग्राम सॉफ़्टवेयर और हार्डवेयर विफलताओं को दूर कर सकते हैं तो हम प्रतिस्पर्धी व्यावसायिक लाभ वाली सेवाएँ प्रदान कर सकते हैं - यह एक और इंजीनियरिंग कार्य है जिसे हम संबोधित करना शुरू कर सकते हैं।
लक्ष्य से लैस होकर, हम गैर-क्रमबद्ध दृष्टिकोण के साथ प्रयोग शुरू कर सकते हैं।
आइए छद्म कोड के इस हिस्से को देखें:
```
def fetch_coordinates(poi: str) -> Point:
…
def find_pois(center: Point, distance: int) -> List[str]:
…
def get_my_location() -> Point:
…
def fetch_coordinates(p) - Point:
…
def main():
me = get_my_location()
for point in find_pois(me, 500):
loc = fetch_coordinates(point)
sys.stdout.write(f“Name: {point} is at x={loc.x} y={loc.y}”)
हम कोड को ऊपर से नीचे तक पढ़ सकते हैं और उचित रूप से मान सकते हैं कि `find_pois` फ़ंक्शन को `get_my_location` के बाद कॉल किया जाएगा। और हम अगला POI लाने के बाद पहले POI के निर्देशांक लाएंगे और लौटाएंगे। वे धारणाएँ सही हैं और कार्यक्रम के बारे में एक मानसिक मॉडल, तर्क बनाने की अनुमति देती हैं।
आइए कल्पना करें कि हम अपने कोड को गैर-क्रमिक रूप से निष्पादित करने के लिए बना सकते हैं। ऐसे कई तरीके हैं जिनसे हम वाक्यात्मक रूप से ऐसा कर सकते हैं। हम स्टेटमेंट रीऑर्डरिंग के प्रयोगों को छोड़ देंगे (आधुनिक कंपाइलर और सीपीयू यही करते हैं) और अपनी भाषा का विस्तार करेंगे ताकि हम एक नए फ़ंक्शन निष्पादन शासन को व्यक्त करने में सक्षम हों:
धागे अलग-अलग स्वादों में आते हैं: एक POSIX धागा, हरा धागा, कोरटाइन, गोरौटाइन। विवरण बहुत भिन्न होते हैं, लेकिन यह कुछ ऐसा होता है जिसे क्रियान्वित किया जा सकता है। यदि कई फ़ंक्शन एक साथ चल सकते हैं तो प्रत्येक को अपनी स्वयं की शेड्यूलिंग इकाई की आवश्यकता होती है। यानी कि मल्टी-थ्रेडिंग से आता है, एक के बजाय, हमारे पास निष्पादन के कई थ्रेड हैं। कुछ वातावरण (एमपीआई) और भाषाएं अंतर्निहित रूप से थ्रेड बना सकते हैं, लेकिन आमतौर पर हमें इसे सी में `pthread_create`, पायथन में `थ्रेडिंग` मॉड्यूल क्लास या गो में एक सरल `go` स्टेटमेंट का उपयोग करके स्पष्ट रूप से करना होगा। कुछ सावधानियों के साथ हम समान कोड को अधिकतर समानांतर में चला सकते हैं:
def fetch_coordinates(poi, results, idx) -> None: … results[idx] = poi def main(): me = get_my_location() points = find_pois(me, 500) results = [None] * len(points) # Reserve space for each result threads = [] for i, point in enumerate(find_pois(me, 500)): # i - index for result thr = threading.Thread(target=fetch_coordinates, args=(poi, results, i)) thr.start() threads.append(thr) for thr in threads: thr.wait() for point, result in zip(points, results): sys.stdout.write(f“Name: {poi} is at x={loc.x} y={loc.y}”)
हमने अपना प्रदर्शन लक्ष्य हासिल कर लिया है: जैसे-जैसे कोर की संख्या बढ़ती है और तेजी से खत्म होती है, हमारा प्रोग्राम कई सीपीयू और स्केल पर चल सकता है। अगला इंजीनियरिंग प्रश्न हमें अवश्य पूछना चाहिए: किस कीमत पर?
हमने जानबूझकर क्रमबद्ध और पूर्वानुमानित निष्पादन को छोड़ दिया। वहाँ है
अगला परिणाम यह है कि इस बार एक कार्य दूसरे से पहले समाप्त हो सकता है, अगली बार यह दूसरे तरीके से हो सकता है। निष्पादन की यह नई व्यवस्था डेटा दौड़ की ओर ले जाती है: जब समवर्ती फ़ंक्शन डेटा के साथ काम करते हैं तो इसका मतलब है कि डेटा पर लागू संचालन का क्रम अपरिभाषित है। हम डेटा दौड़ का सामना करना शुरू करते हैं और इसका उपयोग करके उनसे निपटना सीखते हैं:
इस बिंदु पर, हमें कम से कम दो चीज़ें पता चलती हैं। सबसे पहले, डेटा तक पहुंचने के कई तरीके हैं। कुछ डेटा है
जब हम तर्क की इस पंक्ति को जारी रखते हैं, तो थ्रेड-लोकल स्टोरेज जैसी अन्य तकनीकें स्वाभाविक रूप से आती हैं। हमने अभी अपने प्रोग्रामिंग टूलबेल्ट में एक नया गैजेट हासिल किया है, जो सॉफ्टवेयर बनाकर हम जो हासिल कर सकते हैं उसका विस्तार कर रहे हैं।
हालाँकि, एक ऐसी अपरिवर्तनीयता है जिस पर हम अभी भी भरोसा कर सकते हैं। जब हम किसी थ्रेड से साझा (दूरस्थ) डेटा तक पहुंचते हैं, तो हमें वह हमेशा मिलता है। ऐसी कोई स्थिति नहीं है जब कुछ मेमोरी खंड उपलब्ध न हो। यदि बैकिंग भौतिक मेमोरी क्षेत्र में खराबी आती है तो ओएस प्रक्रिया को बंद करके सभी प्रतिभागियों (थ्रेड्स) को समाप्त कर देगा। यही बात "हमारे" थ्रेड पर भी लागू होती है यदि हमने म्यूटेक्स को लॉक कर दिया है, तो ऐसा कोई रास्ता नहीं है कि हम लॉक खो दें और हम जो कर रहे हैं उसे तुरंत रोकना होगा। हम इस अपरिवर्तनीय (ओएस और आधुनिक हार्डवेयर द्वारा लागू) पर भरोसा कर सकते हैं कि सभी प्रतिभागी या तो मृत हैं या जीवित हैं। सभी का भाग्य समान है : यदि प्रक्रिया (ओओएम), ओएस (कर्नेल बग) या हार्डवेयर में कोई समस्या आती है - तो हमारे सभी थ्रेड बाहरी बचे हुए दुष्प्रभावों के बिना एक साथ मौजूद नहीं रहेंगे।
एक महत्वपूर्ण बात ध्यान देने योग्य है. हमने धागों की शुरुआत करके यह पहला कदम कैसे उठाया? हम अलग हो गए, विभाजित हो गए। एक शेड्यूलिंग इकाई के बजाय, हमने एकाधिक की शुरुआत की। आइए इस अनसाझाकरण दृष्टिकोण को लागू करना जारी रखें और देखें कि यह कैसे होता है। इस बार हम प्रोसेस वर्चुअल मेमोरी को कॉपी करते हैं। इसे कहते हैं - एक प्रक्रिया को जन्म देना । हम अपने प्रोग्राम का दूसरा उदाहरण चला सकते हैं या अन्य मौजूदा उपयोगिता शुरू कर सकते हैं। यह इसके लिए एक बढ़िया दृष्टिकोण है:
लगभग सभी ==
यह निष्पादन का एक और तरीका है जिसे हमने साझा भाग्य अपरिवर्तनीय को छोड़कर और वर्चुअल मेमोरी को अनशेयर करके और एक प्रतिलिपि बनाकर खोजा है। प्रतिलिपियाँ मुफ़्त नहीं हैं:
यहाँ क्यों रुकें? आइए जानें कि हम अपने प्रोग्राम को और किन लोगों के बीच कॉपी और वितरित कर सकते हैं। लेकिन सबसे पहले वितरण क्यों किया जाए? कई मामलों में हाथ में मौजूद कार्यों को एक ही मशीन का उपयोग करके हल किया जा सकता है।
हमें वितरित होने की जरूरत है
कुछ नाम है:
ओएस अपग्रेड: समय-समय पर हमें अपनी मशीनों को रीबूट करने की आवश्यकता होती है
हार्डवेयर विफलताएँ: वे हमारी अपेक्षा से अधिक बार घटित होती हैं
बाहरी विफलताएँ: बिजली और नेटवर्क आउटेज एक चीज़ है।
यदि हम किसी OS की प्रतिलिपि बनाते हैं - तो हम उसे एक वर्चुअल मशीन कहते हैं और ग्राहकों के प्रोग्राम को भौतिक मशीन पर चला सकते हैं और उस पर एक बड़ा क्लाउड व्यवसाय बना सकते हैं। यदि हम दो या दो से अधिक कंप्यूटर लेते हैं और प्रत्येक पर अपना प्रोग्राम चलाते हैं - तो हमारा प्रोग्राम हार्डवेयर विफलता पर भी जीवित रह सकता है, 24/7 सेवा प्रदान कर सकता है और प्रतिस्पर्धात्मक लाभ प्राप्त कर सकता है। बहुत पहले बड़े निगम और भी आगे बढ़ गए थे और अब इंटरनेट दिग्गज विभिन्न डेटासेंटर और यहां तक कि महाद्वीपों में प्रतियां चलाते हैं, इस प्रकार एक कार्यक्रम को तूफान या साधारण बिजली आउटेज के लिए लचीला बनाते हैं।
लेकिन यह स्वतंत्रता एक कीमत पर आती है: पुरानी अपरिवर्तनीयताओं को लागू नहीं किया जाता है, हम अपने दम पर हैं। कोई चिंता नहीं, हम पहले व्यक्ति नहीं हैं। हमारी सहायता के लिए बहुत सारी तकनीकें, उपकरण और सेवाएँ हैं।
हमने हाल ही में सिस्टम और उनकी संबंधित निष्पादन व्यवस्थाओं के बारे में तर्क करने की क्षमता हासिल की है। प्रत्येक बड़े पैमाने पर सिस्टम के अंदर अधिकांश हिस्से परिचित अनुक्रमिक और स्टेटलेस होते हैं, कई घटक मेमोरी प्रकारों और पदानुक्रमों के साथ बहु-थ्रेडेड होते हैं, जो कुछ वास्तविक रूप से वितरित भागों के मिश्रण द्वारा एक साथ रखे जाते हैं:
लक्ष्य यह भेद करने में सक्षम होना है कि हम वर्तमान में कहां हैं, अपरिवर्तनीय क्या धारण करते हैं और तदनुसार कार्य (संशोधित/डिज़ाइन) करते हैं। हमने "अज्ञात अज्ञात" को "ज्ञात अज्ञात" में बदलने के मूल तर्क पर प्रकाश डाला। इसे हल्के में न लें, यह एक महत्वपूर्ण प्रगति है।