4-chan Go प्रोग्रामर
(dolthub.com)- 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 किया जाता है, और अंत मेंintvalues को जोड़कर 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चलाने पर programi 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 sendChanChanChan3-channel producer को goroutine के रूप में शुरू करता है
- उदाहरण में
- consumer पक्ष भी हर layer में आए channel को लेकर अगले चरण के consumer को
factorबार शुरू करता हैreceiveChanChanChan_4chanसे_3chanलेकर 3-channel consumer शुरू करता है
अंतिम layer में value transfer और sum
- सबसे निचली layer में अब channel नहीं बल्कि वास्तविक
intvalues भेजी जाती हैं sendfunction_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 टिप्पणियां
Hacker News की राय
असली professional software engineers के साथ करीब से काम करने वाले एक वैज्ञानिक के तौर पर, उनके बहुत-से काम मुझे इसी तरह दिखते हैं, इसलिए समझना बेहद मुश्किल होता है कि वे ऐसा क्यों करते हैं
मैंने देखा है कि code की एक line असल में call होने से पहले क्रम से 4 interface functions से गुजरती है, और वे functions अलग-अलग folders की अलग-अलग files में बिखरे होते हैं
इसलिए code क्या कर रहा है, यह पढ़ना थका देता है, और कुछ levels अंदर जाने पर शक होने लगता है कि क्या मैं सही जगह देख रहा हूं, और क्या कभी उस जगह तक पहुंचूंगा जहां असली calculation होती है
यह अहसास कि यह जरूरत से ज्यादा और 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 करने जितना बड़ा है?” पर हां कह सकते हैं
जहां समझ और क्षमता होनी चाहिए, वहां कभी-कभी डराने वाली कमी होती है
undergraduate के समय मैंने एक computer science PhD के साथ लगभग 30 मिनट pair programming की थी, और वह काफी आंखें खोलने वाला अनुभव था
उन्हें software की बिल्कुल समझ नहीं थी; वे यहां तक point out कर रहे थे कि standard library data structure का size negative नहीं है, यह check क्यों नहीं किया जा रहा
हालांकि कभी-कभी ऐसी structure के पीछे वजह भी होती है। कभी यह सच में reasonable होता है, और कभी यह उन लोगों द्वारा पहले से बनाए गए पागलपन भरे codebase से निपटने का तरीका भी होता है
मैंने research papers में इस्तेमाल code पढ़ा है; theoretical math अक्सर समझ से परे होता है, लेकिन logic देखने के लिए code में जाएं तो कई बार यह उससे भी ज्यादा खराब और unreadable निकलता है
अंत में, हम अपने तरीके के आदी होते हैं, और दूसरा तरीका अजीब लगता है
फिर भी, extra indirection layers आम तौर पर पूरे architecture के भीतर justify होती हैं, और अगर मामला reasonable हो तो सिर्फ local तौर पर देखकर उनकी value समझना मुश्किल हो सकता है
“शुरुआती programming languages का हल्का स्पर्श और शांति हमेशा सुखद होती है। text ज्यादा नहीं होता, पर बहुत काम हो जाता है। पुराने programs compiler से बहस जैसे नहीं, बल्कि एक articulate researcher और अच्छी तरह trained machine colleague के बीच शांत बातचीत जैसे पढ़े जाते हैं। किसे पता था कि sophistication इतनी noise खरीद लाएगी?” — Dick Gabriel
आप दोनों 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 को भेजना बहुत सामान्य हैलेकिन वह ऐसा नहीं था; वह 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” करने की कोई खास वैल्यू नहीं
[0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
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 कोreplychannel में डालनालेकिन उपयोगिता दो 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...
Joe Armstrong का “My favorite Erlang Program” याद आता है
https://joearms.github.io/published/2013-11-21-My-favorite-e...