1 पॉइंट द्वारा GN⁺ 2024-08-29 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • DoltHub ने Go concurrency में आम तौर पर इस्तेमाल होने वाले channels को जानबूझकर ज़रूरत से ज़्यादा nested करके, channel को channel के भीतर भेजने वाला एक मज़ाकिया उदाहरण बनाया
  • वास्तव में inherited code में chan chan struct{} मौजूद था, और इसे worker goroutine में नया channel पहुँचाने वाले fan-out pattern के लिए इस्तेमाल किया गया था, लेकिन इसे समझना और संभालना कठिन होने के कारण बाद में फिर से लिखा गया
  • यह उदाहरण C-परिवार के int**** “4-star programmer” मज़ाक को Go के chan तक बढ़ाता है, और _4chan:= make(chan chan chan chan int) को top-level channel के रूप में इस्तेमाल करता है
  • factor = 3 पर हर channel layer में producer और consumer को branch किया जाता है, और अंत में int values को जोड़कर 3 की 5वीं घात 243 प्रिंट की जाती है
  • वास्तविक उपयोग में implementation और debugging की कठिनाई, channel बंद करने की handling, sync.WaitGroup की ज़रूरत, और goroutine leak की वजह से यह उपयुक्त नहीं है; उदाहरण termination logic की जगह time.Sleep() पर निर्भर करता है

Dolt में मिला nested channel का मामला

  • DoltHub दुनिया का पहला version-controlled SQL database Dolt को Go में लिख रहा है
  • आम Go codebase की तरह यह concurrency implementation के लिए channels और goroutines का उपयोग करता है
  • concurrency programming अपने आप में कठिन होती है, इसलिए आम तौर पर channels और goroutines को सरल और सहज तरीके से संभाला जाता है
  • एक समय दूसरे open source project से लाए गए code में नीचे जैसा channel के भीतर channel था
    • var c chan chan struct{}
  • यह संरचना goroutine के बीच channels पास करके worker goroutine का fan-out pattern लागू करने का तरीका थी
    • बीच का channel नए बनाए गए channel को असली काम करने वाले worker तक पहुँचाने वाले मध्यस्थ की भूमिका निभाता था
    • यह काम तो करता था, लेकिन खासकर goroutine leak को ध्यान में रखने पर इसे समझना और संभालना कठिन था
    • उस code को फिर से लिखा गया और chan chan struct{} हटा दिया गया

“4-star programmer” मज़ाक का Go संस्करण

  • जब C और उससे निकली भाषाएँ व्यापक रूप से इस्तेमाल होती थीं, तब pointer को समझने में कठिनाई महसूस करने वाले शुरुआती लोगों के लिए “4-star programmer” जैसा मज़ाक किया जाता था
  • इसका एक प्रतिनिधि उदाहरण int**** जैसी code शैली है, जिसमें कई स्तरों की pointer indirection होती है
  • Go भी काफी हद तक C से निकली है, इसलिए pointer के साथ उसी तरह का code लिखा जा सकता है
    • *int, **int, ***int, ****int को क्रम से पास किया जाता है
    • अंतिम function में ****i = 100 चलाने पर program i is now 100 प्रिंट करता है
  • Go में C में न होने वाला chan है, इसलिए इसी मज़ाक को channel indirection तक बढ़ाया जा सकता है

4-स्तरीय channel से 5वीं घात निकालना

  • top-level channel इस तरह घोषित किया जाता है
    • _4chan := make(chan chan chan chan int)
  • Go identifiers संख्या से शुरू नहीं हो सकते, इसलिए उदाहरण में _4chan नाम का इस्तेमाल किया गया है
  • _4chan में भेजी जाने वाली value 3-स्तरीय channel होती है
    • _3chan := make(chan chan chan int)
  • इसी तरह layer दर layer नीचे जाते हुए अंत में value channel chan int तक पहुँचा जाता है
  • हर indirection layer में constant factor के आधार पर producer बनाए जाते हैं
    • उदाहरण में const factor = 3
    • sendChanChanChan 3-channel producer को goroutine के रूप में शुरू करता है
  • consumer पक्ष भी हर layer में आए channel को लेकर अगले चरण के consumer को factor बार शुरू करता है
    • receiveChanChanChan _4chan से _3chan लेकर 3-channel consumer शुरू करता है

अंतिम layer में value transfer और sum

  • सबसे निचली layer में अब channel नहीं बल्कि वास्तविक int values भेजी जाती हैं
  • send function _2chan में _1chan भेजने के बाद factor की संख्या में int producer शुरू करता है
  • हर int producer फिर factor की संख्या में goroutine बनाकर _1chan <- 1 चलाता है
  • consumer प्राप्त integer को global sum में जोड़ता है
    • sum को atomic.Int32 के रूप में घोषित किया गया है
    • receive(c chan int) channel से value लेकर sum.Add(int32(s)) चलाता है

execution result और branching की संख्या

  • पूरा program _4chan बनाता है, sending layer और receiving layer को अलग-अलग goroutine के रूप में शुरू करता है, फिर 500 * time.Millisecond तक इंतज़ार करता है
  • उदाहरण का output इस प्रकार है
    • 3 ^ 5: 243
  • यह program संख्या की 5वीं घात को यथासंभव सबसे अधिक distributed तरीके से निकालने वाला एक generalized example है
  • runnable example Go Playground में देखा जा सकता है, और syntax highlighting वाला संस्करण GitHub Gist में है
  • बड़ा factor इस्तेमाल करने पर execution पूरा होने देने के लिए Sleep समय बढ़ाना पड़ सकता है
  • log चालू करने पर हर channel production और consumption layer की branching count देखी जा सकती है
    • starting 3chan producer: 3 बार
    • starting 2chan producer: 9 बार
    • starting 3chan consumer: 9 बार
    • starting 2chan consumer: 27 बार
    • starting chan producer: 27 बार
    • starting 1chan consumer: 81 बार
    • starting int producer: 81 बार
    • sending int: 243 बार
    • received int: 243 बार

वास्तविक code में इससे बचने की वजह

  • यह तरीका वास्तविक code में इस्तेमाल करने के लिए implementation और debugging दोनों में कठिन है
  • channel को channel के भीतर भेजने पर यह तय करना मुश्किल हो जाता है कि किस channel को कब बंद किया जाए
  • वास्तविक use case में channels को बंद करना ज़रूरी होगा, लेकिन termination logic जोड़ने के लिए यह track करना पड़ेगा कि सभी channel send पूरे हुए या नहीं
  • termination handling लागू करने के लिए जगह-जगह sync.WaitGroup जोड़ना पड़ता, और तब यह मज़ाकिया example पढ़ने में कठिन हो जाता
  • अंतिम example ने termination logic की जगह time.Sleep() का इस्तेमाल करके, और कई goroutine leak छोड़कर, चीज़ों को सरल बनाया

1 टिप्पणियां

 
GN⁺ 2024-08-29
Hacker News की राय
  • असली professional software engineers के साथ करीब से काम करने वाले एक वैज्ञानिक के तौर पर, उनके बहुत-से काम मुझे इसी तरह दिखते हैं, इसलिए समझना बेहद मुश्किल होता है कि वे ऐसा क्यों करते हैं
    मैंने देखा है कि code की एक line असल में call होने से पहले क्रम से 4 interface functions से गुजरती है, और वे functions अलग-अलग folders की अलग-अलग files में बिखरे होते हैं
    इसलिए code क्या कर रहा है, यह पढ़ना थका देता है, और कुछ levels अंदर जाने पर शक होने लगता है कि क्या मैं सही जगह देख रहा हूं, और क्या कभी उस जगह तक पहुंचूंगा जहां असली calculation होती है

    • यह वाकई खराब practice है, और यह उस तरीके के करीब है जिसमें कोई बेहद जोशीला junior engineer software लिखता है
      यह अहसास कि यह जरूरत से ज्यादा और confusing लग रहा है, गलत नहीं है; जब आप पहली बार “interesting” code लिखते हैं, तो यह technically complex और यहां तक कि elegant भी दिख सकता है, लेकिन जिस software को असल में बड़ा होना होता है, उसमें यह technical nightmare बन जाता है
      मैंने Go code में channels के misuse को करीब 2 साल तक साफ किया है; channels की सच में जरूरत कम ही पड़ती है, लेकिन शुरुआत में उन्हें कई कामों के लिए आसानी से इस्तेमाल किया जा सकता है, यही समस्या है
      channel सही तरीके से इस्तेमाल हो रहा है या नहीं, इसका पैमाना यह है कि क्या आप “क्या इसे direct function call से नहीं किया जा सकता?”, “क्या इसे wait group या mutex से नहीं किया जा सकता?” पर नहीं कह सकते हैं, और “क्या concurrency/parallelism का फायदा concurrent code debug करने की complexity को justify करने जितना बड़ा है?” पर हां कह सकते हैं
    • इससे भी बदतर चीजें हैं। यह बहुत ज्यादा बढ़ा-चढ़ाकर दिया गया उदाहरण नहीं है: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
      जहां समझ और क्षमता होनी चाहिए, वहां कभी-कभी डराने वाली कमी होती है
      undergraduate के समय मैंने एक computer science PhD के साथ लगभग 30 मिनट pair programming की थी, और वह काफी आंखें खोलने वाला अनुभव था
      उन्हें software की बिल्कुल समझ नहीं थी; वे यहां तक point out कर रहे थे कि standard library data structure का size negative नहीं है, यह check क्यों नहीं किया जा रहा
      हालांकि कभी-कभी ऐसी structure के पीछे वजह भी होती है। कभी यह सच में reasonable होता है, और कभी यह उन लोगों द्वारा पहले से बनाए गए पागलपन भरे codebase से निपटने का तरीका भी होता है
    • coders के spectrum के एक छोर पर वैज्ञानिक हैं, दूसरे छोर पर software engineers। हमें बचा सकती है तो सिर्फ balance
      मैंने research papers में इस्तेमाल code पढ़ा है; theoretical math अक्सर समझ से परे होता है, लेकिन logic देखने के लिए code में जाएं तो कई बार यह उससे भी ज्यादा खराब और unreadable निकलता है
      अंत में, हम अपने तरीके के आदी होते हैं, और दूसरा तरीका अजीब लगता है
    • overengineering एक आम वजह है। सरल solution अक्सर छिपा होता है और ढूंढना मुश्किल होता है
      फिर भी, extra indirection layers आम तौर पर पूरे architecture के भीतर justify होती हैं, और अगर मामला reasonable हो तो सिर्फ local तौर पर देखकर उनकी value समझना मुश्किल हो सकता है
      “शुरुआती programming languages का हल्का स्पर्श और शांति हमेशा सुखद होती है। text ज्यादा नहीं होता, पर बहुत काम हो जाता है। पुराने programs compiler से बहस जैसे नहीं, बल्कि एक articulate researcher और अच्छी तरह trained machine colleague के बीच शांत बातचीत जैसे पढ़े जाते हैं। किसे पता था कि sophistication इतनी noise खरीद लाएगी?” — Dick Gabriel
    • उलटे, मैं इस बात से सहमत हूं कि जब software engineers अच्छी तरह नहीं जानते, तो वे abstraction को बहुत दूर तक ले जाते हैं, लेकिन जो लोग पेशेवर रूप से software engineers नहीं हैं, उनके बनाए code को भी मैं खास ऊंचा नहीं मानता
      आप दोनों extremes देखते हैं। बहुत ज्यादा abstraction के कारण जरूरत से ज्यादा बिखरे codebases, और बिल्कुल abstraction न होने के कारण सिर्फ काम निकालने वाली scripts जैसे codebases—दोनों पर काम करना मुश्किल होता है
      Python, JS, PHP scripts में से बहुत-सी “छोड़ो, मुझे बस मेरा desired result दे दो” वाले attitude से लिखी गई थीं, और जो लोग रोज code में काम करते हैं, उन्हें collaboration और resilience में मदद करने वाली abstractions चाहिए
  • शुरुआत वाला meme, एक recovering C programmer के तौर पर, सच में हंसाने वाला था
    language को इस तरह मोड़कर इस्तेमाल करने के examples देखना मजेदार है; C में ऐसे मौके भरे पड़े हैं, और Go में भी ऐसा देखना interesting है

  • कहा गया है, “C और उसकी derived languages के दबदबे वाले दिनों का पुराना programming joke”, लेकिन हम अभी भी उसी दौर में जी रहे हैं

  • इस thread में इसे abstraction की अधिकता का classic example बताकर आलोचना करना ironic है
    C में तीन-star variables कम दिखने की वजह यह नहीं है कि pointer chains rare हैं, बल्कि यह है कि एक साथ दो से ज्यादा levels manipulate करने की जरूरत कम पड़ती है
    Python, Java, JavaScript जैसी languages, जहां ज्यादातर चीजें default रूप से pointer जैसी होती हैं, उनमें length 4 से काफी लंबी pointer chains निश्चित रूप से होती हैं
    गहरे हिस्से आम तौर पर ऐसे structures के अंदर छिपे होते हैं जिनकी internals की चिंता नहीं करनी पड़ती—यानी abstracted होते हैं
    channels भी concurrent code में लगभग वही भूमिका निभाते हैं जो sequential code में pointers निभाते हैं, इसलिए अगर यहां कोई fatal flaw है, तो वह abstraction की अधिकता नहीं बल्कि शायद abstraction की कमी हो सकती है
    फिर भी codebase को नहीं जानता, इसलिए शायद chan chan ठीक वही abstraction रही हो जो वे लिखना चाहते थे। Erlang जैसी concurrency-heavy languages में response पाने वाले process की पहचान कराने के लिए PID को किसी दूसरे PID को भेजना बहुत सामान्य है

    • career की शुरुआत में मुझे तीन-star commit के लिए डांट पड़ी थी, यह कहते हुए कि pointer के pointer के pointer की किसे जरूरत होती है
      लेकिन वह ऐसा नहीं था; वह strings की array का address था। C में ऐसा ही था, और करीब 20 साल बाद भी मुझे लगता है कि वह सबसे intuitive solution था
      colleague के बचाव में कहूं तो, शायद comments नहीं थे। वह हिस्सा मेरी जिम्मेदारी थी
  • Buena Vista Social Club का timeless classic याद आ गया https://www.youtube.com/watch?v=o5cELP06Mik

  • chan chan Value या chan struct{resp chan Value} ऐसे patterns हैं जिन्हें मैंने बहुत खास स्थितियों में सच में इस्तेमाल किया है
    message bus भी इस्तेमाल किया जा सकता था, लेकिन तब फिर message bus को संभालना पड़ता

    • chan chan काफ़ी अक्सर इस्तेमाल होता है। यह वह स्थिति है जहाँ किसी internal server या actor को message भेजते समय, response पाने वाला channel भी उसी message में डालकर भेजा जाता है
      असल में यह grep से मिल जाने वाले शाब्दिक chan chan से ज़्यादा chan struct { ... कोई चीज़ जिसमें channel हो ... } के रूप में होता है, लेकिन सिद्धांत वही है
      यह बहुत उपयोगी pattern है और मैं इसे Go की बुनियादी चीज़ों में से एक मानता हूँ
      हालांकि जहाँ तक मुझे पता है, chan chan chan मैंने कभी इस्तेमाल नहीं किया
      ऐसी हर चीज़ को message bus में बदल देना ज़रूरत से ज़्यादा है। performance characteristics भी बहुत अलग हैं, और Go के channels operating system process के अंदरूनी components जैसे हैं, इसलिए अगर communication process के अंदर ही है तो उसे message bus में “upgrade” करने की कोई खास वैल्यू नहीं
    • शायद यह gopl के concurrent chat server जैसे pattern जैसा होगा। शुरुआत में थोड़ा चौंकाता है, लेकिन फिर भी काफ़ी पढ़ने लायक है
      [0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
    • मैंने भी इसे इस्तेमाल किया है। Go में Promise जैसी semantics लागू करने के लिए उपयोगी है
  • channels का channel एक सामान्य pattern है, लेकिन आम तौर पर यह struct value के channel के रूप में ज़्यादा दिखता है, और उस struct type में channel field होता है
    इसका इस्तेमाल ऐसे requests भेजने के लिए किया जा सकता है जिन्हें पूरा होना है, और फिर उन values का क्रम की परवाह किए बिना इंतज़ार करने के लिए, ताकि head-of-line blocking से बचा जा सके
    उदाहरण के लिए, type request struct { params, reply chan response } की तरह request को channel से भेजना, और worker के काम पूरा करने के बाद result को reply channel में डालना
    लेकिन उपयोगिता दो levels तक ही रही, और मैंने ऐसा कोई use case नहीं देखा जहाँ chan chan chan की ज़रूरत पड़े

  • एक counterexample blog है जहाँ channel भेजने वाले channel से dynamic dispatch implement किया गया है। यह Go नहीं बल्कि Limbo है, लेकिन concept वही है। शायद complexity उलटे मुद्दे को साबित करती है https://ipn.caerwyn.com/2007/07/lab-78-dynamic-dispatch.html...

    • जिज्ञासु लोगों के लिए जोड़ दूँ, Limbo Go का एक तरह का precursor language है और Plan 9 के descendant Inferno operating system में इस्तेमाल होता था
  • Joe Armstrong का “My favorite Erlang Program” याद आता है
    https://joearms.github.io/published/2013-11-21-My-favorite-e...