4 पॉइंट द्वारा GN⁺ 2024-03-26 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Rust का async/await थ्रेड्स का सिर्फ़ एक साधारण विकल्प नहीं है, बल्कि I/O-केंद्रित concurrency code को composable state machines के रूप में व्यक्त करने वाला एक programming model है
  • web server की तरह कई connections को एक साथ संभालने वाले code में सिर्फ़ linear execution से जल्दी सीमाएँ सामने आती हैं, जबकि थ्रेड्स thread::spawn के ज़रिए client handling को अलग कर concurrent processing संभव बनाते हैं
  • async/await await बिंदु पर execution छोड़ देता है, और executor दूसरे कामों को आगे चलाता है, जिससे एक ही runtime के भीतर बहुत-से tasks interleave होकर चल सकते हैं
  • 3-second timeout जैसी ज़रूरत async में race और Timer के संयोजन से जोड़ी जा सकती है, लेकिन synchronous thread code में TcpStream-विशेष wrapper और read/write timeout settings की ज़रूरत पड़ती है, इसलिए इसकी generality कम हो जाती है
  • केवल performance overhead के आधार पर async को समझाने पर CPU bound कामों में counterexample मिलते हैं; Rust async की असली ताकत semantic expressiveness और ecosystem composability में है

Rust concurrency समस्या की शुरुआत

  • Rust का सामान्य code मूल रूप से linear execution structure पर चलता है
    • foo(), bar(), baz() की तरह एक काम पूरा होने के बाद अगला काम चलता है
  • लेकिन web server की तरह कई काम एक साथ संभालने पड़ें तो linear structure जल्दी अपनी सीमा पर पहुँच जाता है
    • TcpListener::accept() से client लेने और handle_client() चलाने वाली संरचना में पहले client को process करते समय दूसरे client को इंतज़ार करना पड़ता है
    • अगर handle_client() को कुछ milliseconds लगते हों और एक साथ clients 2 हों, तो थोड़ी प्रतीक्षा पैदा होती है
    • अगर एक साथ clients 20 लाख हों, तो queue के आख़िर में मौजूद user को कई मिनट इंतज़ार करना पड़ सकता है

थ्रेड्स इसे कैसे हल करते हैं

  • operating system thread registers के मान और program stack को memory में सहेजकर किसी दूसरी routine को चला सकता है, और बाद में मूल routine को फिर से resume कर सकता है
  • web server code thread::spawn(move || handle_client(client)) जैसे रूप में client handling को अलग thread पर भेज देता है
    • main thread नए connections को लगातार accept() करता रहता है
    • अगर client-handling thread block हो जाए, तो OS main thread पर लौटकर अगला connection ले सकता है
    • दो clients कुछ microseconds की देरी के बाद parallel चल सकते हैं
  • अगर production-grade web server के पास दर्जनों CPU cores हों, तो OS threads को सिर्फ़ साथ-साथ चलते हुए दिखाता ही नहीं, बल्कि वास्तव में कई threads को एक साथ चला भी सकता है

async/await कैसे काम करता है

  • user-space concurrency के लिए event-based programming, actor, coroutine जैसे कई models हैं, और Rust ने async/await को चुना है
  • सरल रूप में देखें तो program को स्वतंत्र रूप से चल सकने वाली state machines के समूह के रूप में compile किया जाता है
    • async fn पारंपरिक function नहीं, बल्कि state machine लौटाने वाला function है
    • await किसी दूसरी state machine को मौजूदा state machine के एक चरण के रूप में शामिल करता है
    • जब अंदर का function नए connection का इंतज़ार करने जैसी स्थिति में execution छोड़ता है, तो पूरी state machine ऊपर के executor को control लौटा देती है
  • smol::Executor जैसा executor मौजूदा state machine की जगह spawn से बनी दूसरी state machine चलाता है
    • async move { handle_client(client).await } block main से स्वतंत्र एक नई state machine है
    • main के yield करते ही clients के कामों में से एक चलता है, और उसके फिर से yield करते ही अगला task चलने लगता है
  • इस संरचना से लाखों clients को एक साथ संभाला जा सकता है, लेकिन executor, task, state machine जैसी अतिरिक्त अवधारणाओं के कारण complexity भी बढ़ती है

timeout उदाहरण में दिखने वाली composability

  • Rust की एक बड़ी ताकत composability है
    • Iterator में कई combinators जोड़े जा सकते हैं, और फिर परिणाम को दोबारा Iterator लेने वाले function में दिया जा सकता है
    • mpsc::channel() के recv.try_iter().filter(...).map(...) की तरह values को filter और transform करके list में जोड़ा जा सकता है
  • async/await यही composability I/O bound functions पर भी लागू करने देता है
  • अगर handle_client() एक asynchronous function है जो read_to_end, do_something_with_data, write_all को await करता है, तो 3-second timeout दो Futures को जोड़कर लागू किया जा सकता है
    • race दो Futures को एक साथ चलाता है
    • Timer तय समय के बाद return करता है
    • मौजूदा connection-handling code को async block में wrap करके, 3 seconds बाद TimedOut error लौटाने वाले Future के साथ race करवा देना पर्याप्त है
  • यह तरीका सिर्फ़ TcpStream तक सीमित नहीं है
    • जो भी impl AsyncRead + AsyncWrite implement करता हो, उस पर यही pattern लागू किया जा सकता है
    • सामान्य stream के ऊपर GZIP stream, Unix socket, file जैसे targets भी बदले जा सकते हैं

synchronous thread code में वही timeout लागू करने की सीमाएँ

  • blocking code में सामान्यतः read या write system call को बीच में रोकना मुश्किल होता है, और file descriptor बंद करने जैसी विधि Rust में उपयोगी नहीं होती
  • TcpStream set_read_timeout और set_write_timeout देता है
    • read और write के लिए अलग-अलग timeout सेट किए जा सकते हैं
    • लेकिन अगर client हर 2.9 seconds पर 1 byte भेजे, तो साधारण timeout बार-बार reset हो सकता है
  • इससे बचने के लिए TcpStream को wrap करने वाला DeadlineStream जैसा type बनाना पड़ता है, जो हर बार पूरे deadline तक बचा समय निकालकर read/write timeout में सेट करे
  • यह तरीका काम कर सकता है, लेकिन इसकी सीमाएँ बड़ी हैं
    • यह TcpStream से बँधा हुआ है
    • Rust में set_read_timeout और set_write_timeout के उपयोग को abstract करने वाला कोई trait नहीं है
    • generic writer पर इसे लागू करने के लिए काफ़ी अतिरिक्त काम चाहिए
    • timeout setting के लिए अतिरिक्त system calls लगते हैं
    • वास्तविक web server logic में इसका उपयोग और भी असुविधाजनक हो सकता है

Rust async ecosystem के उदाहरण

  • HTTP ecosystem ने client सहित async/await को मुख्य runtime mechanism के रूप में इसलिए अपनाया, क्योंकि functions की composability बहुत उपयोगी है
    • HTTP calls बनाने वाले functions को अलग-अलग hooks और use cases में फिट किया जा सकता है
  • tower async/await composability का एक प्रमुख उदाहरण है
    • अगर service को async function के रूप में implement किया जाए, तो timeout, rate limiting, load balancing, hedging, और backpressure handling जोड़ी जा सकती है
    • कौन-सा runtime उपयोग हो रहा है, या service अंदर क्या कर रही है, इससे स्वतंत्र होकर tower लगाकर robustness बढ़ाई जा सकती है
  • macroquad Rust का एक छोटा game engine है, जो main function में async/await का उपयोग करके engine चलाता है
    • Rust में किसी काम का इंतज़ार करने के लिए linear function को रोकना हो, तो उसे व्यक्त करने के लिए async/await उपयुक्त है
    • एक ही thread पर game server network connection और GUI framework को साथ-साथ poll करने जैसी संरचना संभव है

केवल performance के आधार पर async को समझाने की सीमा

  • Rust Async Book तुलना करता है कि OS threads programming model बदले बिना concurrency व्यक्त करना आसान बनाते हैं, लेकिन threads के बीच synchronization कठिन हो सकता है, performance overhead बड़ा होता है, और thread pool के साथ भी बड़े I/O bound workloads को पर्याप्त रूप से support करना कठिन हो सकता है
  • async community में जब पूछा जाता है कि OS threads की बजाय async क्यों, तो अक्सर जवाब होता है: “overhead कम है, बाकी सब वही है”
  • web server लिखने वालों ने async/await अपनाया ताकि C10k problem हल किया जा सके, लेकिन हर user के लिए async/await चुनने का कारण performance होना ज़रूरी नहीं है
  • performance advantage स्थिति के अनुसार गायब भी हो सकता है
    • CPU bound कामों में thread-आधारित workflow, बराबर के async workflow से तेज़ हो सकता है
    • Rust async के अस्थायी performance लाभ को अक्सर ज़रूरत से ज़्यादा उभारा गया है, जबकि semantic लाभ को कम आँका गया है
  • async/await कोई niche use case का tool नहीं, बल्कि ऐसा शक्तिशाली programming model है जो उन patterns को संभाल सकता है जिन्हें synchronous Rust में दर्जनों threads और channels के बिना संक्षेप में व्यक्त करना मुश्किल होता है

sync Rust जैसा बनाने से बेहतर है उसके अंतर को स्वीकार करना

  • Rust project roadmap में यह दिशा है कि async Rust लिखना, कभी-कभी async और await keywords इस्तेमाल करने के अलावा, synchronous code लिखने जितना आसान होना चाहिए
  • लेकिन async Rust को “sync Rust जैसा ही” बनाने की framing मूल रूप से कठिन है, ऐसा भी एक दृष्टिकोण है
    • चाहे 99% तक समान बना दिया जाए, औसत user को अंतर फिर भी महसूस होगा
  • Rust का async/await ecosystem synchronous Rust जैसा बनने की कोशिश करने के बजाय, composability और expressiveness जैसी अपनी ताकतों को और स्पष्ट रूप से सामने लाना चाहिए
  • अगर concurrency की ज़रूरत होने पर async/await को default choice बनाना है, तो इस model को technical performance कारणों से ज़्यादा semantic कारणों से समझाना होगा

1 टिप्पणियां

 
GN⁺ 2024-03-26
Hacker News की राय
  • सिंगल-थ्रेड async/await एक सरल और अच्छी तरह जाना-पहचाना मॉडल है, और JavaScript इसी तरीके पर चलता है
    थ्रेड्स कई CPU को समस्या हल करने में लगाने देते हैं, और Rust लॉक मैनेजमेंट में मदद करता है। अलग-अलग प्राथमिकता वाले थ्रेड भी रखे जा सकते हैं, इसलिए compute-bound कामों के लिए इसकी ज़रूरत पड़ सकती है
    इसके उलट मल्टी-थ्रेड async/await गड़बड़ हो जाता है। जब compute-bound सेक्शन गंभीर रूप से शामिल हो जाएं, तो वे दूसरे कामों के साथ साझा किए गए थ्रेड को असल में ब्लॉक कर देते हैं, इसलिए मॉडल आसानी से टूट सकता है
    Rust का compute-bound मल्टी-थ्रेड उतना अच्छा काम नहीं करता जितनी उम्मीद होती है। store allocator में कई थ्रेड एक ही लॉक पर चोट करते हुए futex congestion collapse पैदा कर सकते हैं, और खासकर buffer विस्तार के दौरान पूरे allocator को लॉक किए रखकर recopy होने पर यह बहुत महंगा हो जाता है। Wine की Microsoft लाइब्रेरी emulation .DLL के अंदर मौजूद library allocator इस समस्या के प्रति संवेदनशील था, जिससे CPU time spinlock में ही चला गया और performance दो अंकों के गुणक से गिर गई, जबकि Microsoft implementation में ऐसा नहीं हुआ
    साथ ही standard Mutex और crossbeam-channel चैनल में unfair mutex starvation हो सकता है। अगर कई थ्रेड किसी resource को lock करके काम करें और unlock करने का चक्र दोहराएं, तो एक थ्रेड लगातार जीत सकता है और बाकी पीछे छूट सकते हैं। fair mutex चाहिए तो parking-lot है, लेकिन इसमें standard mutex की thread panic के समय वाली poisoning safety नहीं है
    अगर यह I/O-bound नहीं है, तो मामला कहीं ज्यादा जटिल हो जाता है
    https://users.rust-lang.org/t/mutex-starvation/89080

    • सही है। मुख्य रूप से I/O-bound गणनाओं को ही संभाला था, लेकिन वहां भी contention problems आते हैं
      अगर I/O throughput ही फिर से सीमा है, तो दस लाख coroutines का क्या मतलब है, ऐसा लगता है। अगर आकार 10 का DB connection pool तुरंत exhaust हो जाए, तो coroutines आपको नहीं बचाएंगे; वे सिर्फ debugging और workarounds को ज्यादा कठिन और reasoning को मुश्किल बनाते हैं
    • लगता है कि इस समस्या को आखिरकार hardware स्तर पर फिर से सोचना पड़ सकता है
      CPU-bound समस्या शायद व्यवस्थित interrupt/resume तक पहुंचती है, और अगर चल रहे n execution threads के लिए fair और efficient queue-based context switching संभव हो, तो उदाहरण के लिए n active contexts वाला CPU होने पर, क्या समस्या resource allocation की समस्या बन सकती है—यह सोचने लायक है
    • समझ नहीं आता कि cooperative multitasking की कठिनाइयों को बार-बार फिर से क्यों खोजा जाता है
      Go, जिसे एक जिम्मेदारी से डिजाइन की गई भाषा माना जाता है, भी शुरुआत में cooperative रास्ते पर गया था, लेकिन आखिरकार उसे preemptive में बदलना ही पड़ा। इसका मतलब यह नहीं कि cooperative multitasking बेकार है, लेकिन उस पर warning label होना चाहिए, और उससे आगे कुछ खास तरह के code को static रूप से चलने से रोकना बेहतर हो सकता है
      संबंधित लेख के तौर पर “What color is your function” जोड़ रहा हूं
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • यह implementation detail है, यह बात बार-बार दोहरानी पड़ती है
      async/await का multi-threaded executor starvation को पर्याप्त रूप से संभाल सकता है, और .NET implementation blocking calls और async को मिलाने वाले बहुत खराब code को भी झेल लेता है
      https://news.ycombinator.com/item?id=39530435
      https://news.ycombinator.com/item?id=39786142
      https://news.ycombinator.com/item?id=39721626
    • async/await द्वारा बनाई जाने वाली colored functions की वजह से software development और maintenance cost भी बढ़ती है
      https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
      अगर software को high scalability की जरूरत नहीं है, तो async के tradeoffs शायद उस लायक न हों
  • async/await और threads की बहस का मूल सवाल यह नहीं है कि कौन-सा पक्ष ज़्यादा जटिल है, बल्कि यह है कि यह ecosystem को दो हिस्सों में बाँट देता है और एक पक्ष second-class citizen बन जाता है, इसलिए प्रोजेक्ट में गलत चुनाव करने पर friction पैदा होता है
    इन्हें मिलाकर इस्तेमाल किया जा सकता है, लेकिन ज़रूरत पड़ने पर यह hacky और inefficient होता है। अभी Rust ecosystem में, अगर I/O शामिल है तो लगभग सब कुछ async/await ecosystem से बंधा हुआ है, और Rust में किए जाने वाले लगभग हर काम में, बहुत कम exceptions छोड़कर, I/O शामिल होता है; इसलिए application का बाकी हिस्सा async चाहता हो या नहीं, non-async libraries को आम तौर पर ignore करना पड़ता है
    अगर Rust ने async/await से ज़्यादा composable abstraction इस्तेमाल किया होता और वह composability बाकी चीज़ों को भी async/await बनाने की मांग न करती, तो लगता है कि ज़्यादातर शिकायतें गायब हो जातीं

    • diagnosis से सहमत हूँ। मेरे Rust async लेख[0] में भी मैंने यही निष्कर्ष निकाला था
      इससे भी बुरी बात यह है कि ecosystem सिर्फ दो हिस्सों में नहीं बंटता, बल्कि async code के अंदर भी आप आम तौर पर executor, अक्सर Tokio, से बहुत मजबूती से बंध जाते हैं। function color problem को बढ़ाकर देखें तो blue (non-I/O), green (blocking I/O), red (async I/O) के बजाय असल में blue, green, red (Tokio), purple (async-std), orange (smol) जैसा हो जाता है
      मुझे इस समस्या का सबसे अच्छा समाधान sans-I/O pattern लगता है। सारे blue code को अलग कर दें और I/O व समय के लिए inversion of control इस्तेमाल करें, तो core protocol logic को I/O से अनजान बनाया जा सकता है और उसे कई I/O forms में wrap करना आसान हो जाता है
      0: https://hugotunius.se/2024/03/08/on-async-rust.html
    • non-async library को तभी ignore करना चाहिए जब चुनने के लिए दो libraries हों और बाकी सब समान हो। ऐसे मामले दुर्लभ होते हैं
      async application में blocking code इस्तेमाल करना उतना smooth नहीं है जितनी उम्मीद होती है, लेकिन मुश्किल भी नहीं है। foo() के बजाय tokio::spawn_blocking(foo).await इस्तेमाल करें; यह नया code अलग thread में चलाता है और उस thread के खत्म होने पर complete होने वाला future लौटाता है
    • C# भी मूल रूप से ऐसा ही है
      I/O के लिए non-async विकल्प मौजूद हैं, लेकिन async विकल्प इस्तेमाल करने पर असल में Main() तक सब कुछ async होने के लिए मजबूर हो जाता है। synchronous method से async method को सुरक्षित रूप से call करने के तरीके भी हैं, लेकिन वे debugging को बेहद कठिन बना देते हैं
  • लेख में कई हिस्से छूट गए हैं
    async/await एक thread context में चलता है, इसलिए locking या synchronization की ज़रूरत नहीं होती, लेकिन CPU cores का उपयोग करने के लिए जब कई threads पर async/await चलाते हैं, तो locking और synchronization फिर से ज़रूरी हो जाते हैं। यह complexity external code में छिपाई जा सकती है। उदाहरण के लिए, एक single DB connection तक access synchronize करने के बजाय हर async task के लिए एक DB connection खोलना आसान है, लेकिन SQLite या PostgreSQL में इसका performance पर असर पड़ सकता है
    async/await में error propagation स्पष्ट नहीं है। खासकर जब async tasks को group करना हो, और Happy Eyeballs इसका typical example है
    अगर बात network I/O की है तो backpressure को भी साथ में address करना चाहिए। CPython की async/await implementation network backpressure की कमी के लिए बदनाम है, और उससे समस्याएँ होती हैं

    • async/await में कई समस्याएँ हैं, लेकिन मेरी सबसे बड़ी शिकायत यही है
      Gang of Four की “Design Patterns” ज़्यादातर C++ की खामियों से बचने की recipe book थी, लेकिन लोगों ने उन patterns को उन languages में भी लागू किया जिनमें वे खामियाँ नहीं थीं
      Rust JavaScript नहीं है और कई threads अच्छी तरह चला सकता है। इसलिए async/await इस्तेमाल करना अनिवार्य नहीं था, और systems language की तरह यह दूसरे solutions आज़मा सकता था
      लेकिन JavaScript programmers को Rust की ओर धकेलना हो तो async/await ज़रूरी था। without.boats ने लिखा था कि “Rust के survive करने का दारोमदार इस feature पर है, इस धारणा के sincere passion के साथ async/await को push किया गया”
      https://without.boats/blog/why-async-rust/
      async/await तकनीकी रूप से Rust में fit बैठता है या नहीं, यह important नहीं था; बात ऐसी दिखती है कि JavaScript programmers async/await से familiar थे, इसलिए Rust में भी async/await होना चाहिए था
    • async/await भी threads की तरह एक concurrency mechanism है, और shared memory access करते समय हमेशा locking चाहिए। locking की ज़रूरत नहीं होती—यह बात कहाँ से आई, समझ नहीं आता
    • async locking के मामले में और डरावना हो सकता है
      कोई code block exclusive access पर निर्भर था और उसमें await नहीं था, इसलिए वह guarantee बनी हुई थी; लेकिन बीच में await जोड़ दें तो code टूट जाता है। threading कम से कम यह code में explicit कराती है कि किस चीज़ पर exclusive access चाहिए
      async का मतलब यह भी है कि आप अपनी thread scheduling खुद manage करना चाहते हैं। I/O बहुत हो और CPU-bound code छोटा हो तो ठीक है, लेकिन CPU-bound code हो, या कभी-कभी भी हो, तो आप scheduler खेलने लगते हैं
    • Node.js इस्तेमाल करने वाली team में शामिल होने पर मुझे backpressure problem का सामना करना पड़ा
      कई services बस ABEND होकर crash हो गईं, और Java background से आए मेरे लिए ऐसी कमी हैरान करने वाली थी। team को इसे ठीक करने का तरीका समझाना भी मुश्किल था
      error propagation की वजह से, अगर choice हो तो मैं async/await इस्तेमाल नहीं करूँगा। solo project हो तो maybe, लेकिन दूसरों के साथ काम करना हो, libraries इस्तेमाल करनी हों और सबको समान समझ पर लाना हो, तो बिल्कुल नहीं
      language-level structured concurrency को अभी गंभीरता से इस्तेमाल नहीं किया है, लेकिन Java के Loom Project से उम्मीद है। देखने में लगता है कि यह इस बहस को meaningless बना देगा
    • Rust में async/await सिर्फ एक thread context में ही नहीं चलता
  • लेख में कुछ समस्याएँ हैं
    उदाहरण सिर्फ एक वेब सर्वर का है, और threads वाला समाधान गलत तरीके से हल किया गया है। साथ ही सवाल ऐसा मानता है मानो लोग async/await के बजाय OS threads चाहते हों
    प्रोग्रामर जो चाहते हैं वह conceptual और semantic threads हैं। यानी sequential logic लिखना और async जैसी अजीब annotation न लगाना। अगर async/await इतना अच्छा है, तो सभी functions को implicitly async बना दें और await की जगह सामान्य function call इस्तेमाल करें—क्यों नहीं? तब असल में आप threads की तरह ही programming कर रहे होंगे
    OS threads महंगे होते हैं क्योंकि उनमें statically allocated stack होता है; हमें चाहिए सस्ते threads, जिन्हें एक ही CPU पर लाखों की संख्या में चलाया जा सके। बस भद्दे async/await शब्दों के बिना। wait event या दूसरे thread के पूरा होने का इंतजार करने वाली classical blocking wait के लिए रह सकता है, लेकिन function calls में यह नहीं चाहिए
    वेब सर्वर वाले उदाहरण पर लौटें तो, driver.race(timeout).await से timeout लागू करने पर, race timeout error बताने के बाद client socket का क्या होता है? क्या वह खुला और connected रहकर leak नहीं होता?
    thread वाले version में timeout को भी threaded_race(client_thread, timeout).wait की तरह async/await जैसा ही बनाया जा सकता है। threaded_race timer के साथ thread के parallel timeout को track करता है, और समय होने पर Java style में client_thread.interrupt() call करता है। Thread.interrupt() thread blocked न हो तो सिर्फ flag set करता है, और अगर I/O call में blocked हो तो InterruptedException throw करता है। यह checked exception है, इसलिए compiler client.read_to_end(&mut data) को try/catch में wrap करने या handle_client में exception declare करने को मजबूर करता है, जिससे programmer client socket बंद करना नहीं भूलता

    • race() के अंदर की values Drop हो जाती हैं और driver खुद बचा रहता है
      अगर type को ज्यों-का-त्यों इस्तेमाल करें तो Rust शिकायत करेगा कि Result handle नहीं किया गया, और अगर future के अंदर नया socket local रूप से बनाया गया था तो वह clean up हो जाएगा
      Rust future की अच्छी बात यह है कि आप उसके आसपास का सारा behavior define कर सकते हैं। उस model के उलट जिसमें सभी functions blocking होते हैं, Rust यह specify कर सकता है कि execution को work queue के next task तक कब टालना है, और explicitly stored state (Future struct) रखकर task को arbitrary रूप से तेज़ी से poll करता है। इसलिए thread की तरह yield करने के लिए sleep() करने की जरूरत नहीं होती, यह तेज़ और reasoning में आसान होता है
      Java का Thread.interrupt भी अंततः sleep loop के करीब ही है, और ज़्यादातर applications के लिए ठीक हो सकता है। लेकिन Rust एक systems language है, इसलिए embedded systems में ऐसा तरीका नहीं रखा जा सकता, और kernel या low-latency applications के लिए भी यह अच्छा नहीं है
    • कुछ programmers sequential logic चाहते हैं, लेकिन बहुत से लोग ठीक इसका उल्टा चाहते हैं
      ज़्यादातर मामलों में यह बहुत मायने नहीं रखता कि यह OS blocking system call है या non-blocking system call, लेकिन मैं जिस program को पढ़ रहा हूँ उसका control flow समझना चाहता हूँ—कहाँ wait हो रहा है और उसे parallel में कैसे चलाया जा सकता है
      बल्कि blocking functions के साथ काम करते समय मैं blocking/block keyword pair चाहता। Blocking calls चुपचाप पूरी चीज़ को धीमा बना सकती हैं, और मैंने UI thread में blocking system call आ जाने से बहुत सारी painfully slow apps देखी हैं
    • सभी functions को implicit async बनाकर normal function call की तरह इस्तेमाल करने वाला तरीका पिछले कई दशकों में कई बार आज़माया जा चुका है। “RPC” खोजकर देख लें
      sync और async को unify करने की सभी कोशिशें fail हुई हैं। एक thread के अंदर चलने वाले code, threads के बीच चलने वाले code, और यहाँ तक कि computers के बीच चलने वाले code में बड़े semantic differences होते हैं। उन्हें abstract करने की कोशिश अंततः कम पड़ती है, इसलिए बेहतर है कि शुरुआत से ही ठीक से सीखा जाए
    • याद है कि withoutboats ने किसी लेख में कहा था कि असली जवाब C के साथ compatibility है
    • poll() और select() से code लिखने का तरीका भी है, लेकिन वह फिर एक अलग तरीका है
  • async/await की इज्जत बचाने की लगभग marketing जैसी campaign देखना दिलचस्प है
    मेरे अनुभव में यह सिर्फ technical mistake नहीं थी, बल्कि community को भी इसकी बड़ी कीमत चुकानी पड़ी। सच में useful language features पर focus करने के बजाय Rust की effort इस confusion में भटक गई
    फिर भी language से अभी भी बहुत उम्मीदें हैं, और मेरे हिसाब से मौजूदा विकल्पों में यह सबसे अच्छी है। बस चिंता है कि यह लड़ाई हमेशा चलती रहेगी
    P.S.: AsyncWrite/AsyncRead example अच्छा दिखता है, लेकिन असल में अगर *nix तक सीमित रहें तो threads और file descriptors से भी वही काम किया जा सकता है

    • firmware में async इस्तेमाल किया है और वह life saver था
      ऐसी generalization के लिए आधार कम है और यह किसी खास workload की तरफ झुकी हुई लगती है
    • Rust को बहुत अच्छी तरह नहीं जानता, इसलिए पता नहीं यह सही है या नहीं, लेकिन अनुभव के तौर पर आजकल HN/reddit पर दिखने वाली Rust चर्चाओं में से 10 में 9 async के आसपास लगती हैं
      async में मेरी बिल्कुल दिलचस्पी नहीं है और मैं Rust के बारे में पढ़ना चाहता हूँ, तो यह काफी खराब लगता है
    • अगर आपको लगता है कि threads poll() से तेज़ हैं, तो जानना चाहूँगा कि कौन सा use case है। मैंने पूरी जिंदगी में ऐसा मामला कभी नहीं देखा
    • यह technical mistake नहीं है; जब ultra-low-latency async code चाहिए हो, तो यह शानदार solution है
      गलती इसे उन ज़्यादातर use cases तक push करने में है जहाँ ऐसी जरूरत नहीं है
    • मुझे जानना है कि async work ने दूसरे useful features छीन लिए, इस दावे का कोई आधार है क्या
      कई प्रमुख Rust projects async पर निर्भर करते हैं, सिर्फ thread-based alternatives की तुलना में बड़े performance improvements के कारण नहीं, बल्कि design characteristics के कारण भी। प्रमुख I/O-bound workloads में ये फायदे आसानी से दिखते हैं। वास्तविक समस्याएँ हल करने वाले smart लोगों ने major crates में async को व्यापक रूप से अपनाया है—यह मेरे हिसाब से मजबूत संकेत है कि async सच में useful language feature है
      लड़ाई मुख्यतः Hacker News और reddit पर होती है, जहाँ जिन लोगों को async की जरूरत नहीं है वे इस बात पर नाराज़ होते हैं कि जिन I/O crates का वे इस्तेमाल करते हैं वे अब async चाहते हैं। यह स्थिति मजेदार नहीं है, यह समझता हूँ, और async में वास्तविक समस्याएँ हैं तथा कुछ अभी भी हल हो रही हैं। यह perfect नहीं है। लेकिन forums में दिखने वाला async विभाजन वास्तविक projects जितना व्यापक या dramatic नहीं लगता
  • एक बड़ी छूटी हुई चीज़ cancellation है
    future को cancel करना बहुत आसान है। इसके उलट thread cancellation whack-a-mole जैसा गंदा मामला है, और जबरन thread रोकना भरोसेमंद नहीं है क्योंकि locks locked ही रह जाने का जोखिम होता है
    Rust के async model में हर future पर बाहर से timeout लगाया जा सकता है। हर leaf I/O function को timeout option support करने की ज़रूरत नहीं होती, न ही उस timeout को पूरे call stack में आगे pass करने की ज़रूरत होती है
    in-progress state management के लिए Rust की best practice Drop guard के साथ इस्तेमाल करें, तो बड़े और जटिल काम भी आसानी से और भरोसेमंद तरीके से cancel किए जा सकते हैं

    • हर future को cancel करना आसान नहीं है, बल्कि हर future को cancel हुआ दिखाना आसान है
      मसलन spawn_blocking का इस्तेमाल करने वाली किसी चीज़ को cancel (drop) करने पर भी वह background में चलती रहती है, और user को पता नहीं चल सकता। thread pool से implement किए गए async file system operations भी cancel करने पर चलते रहते हैं
      ऐसी चीज़ें “मुझे पूरा यकीन है कि कोई भी वह file नहीं लिख रहा, फिर service file in use बताकर fail क्यों हो रही है?” जैसे समझना मुश्किल bugs तक ले जा सकती हैं
    • अगर async future implement किया जा सकता था, तो इसके बजाय cancelable threads भी implement किए जा सकते थे
      समस्या काफी हद तक समरूपी है। system calls मुश्किल हैं, लेकिन चाहे thread में करें या async future में, वही system call करने पर बिल्कुल वही cancellation problem पैदा होती है
    • समझ नहीं आता thread cancellation इतना मुश्किल क्यों है
      सभी threads की पहुँच में रहने वाली किसी flag जैसी state रख दें, और work loop में वह flag check करवाएँ। अगर false हो तो return करें और thread को join कर दें, बस
    • मेरे अनुभव में cancellation इतनी चिंता करने लायक चीज़ नहीं रही
      अगर कोई काम अब उपयोगी नहीं रहा, तो यह जानकारी आखिरकार उस काम की ओर से call किए गए function को दिख जाए, इतना काफी है। RPC शुरू करने जैसे किसी बहुत महंगे काम से ठीक पहले न हो, तो मैं अलग से check नहीं करता
  • मेरे हिसाब से बेहतर सवाल यह है: “fiber क्यों नहीं, async/await क्यों?”
    मुझे पता है कि Rust में 1.0 से पहले green threads थे और उन्हें जानबूझकर हटाया गया था, लेकिन fiber-आधारित concurrency implementation के कई तरीके हैं—जैसे भाषा में भारी runtime embed किए बिना भी इसे करने के तरीके
    अगर मैंने लेख को सही समझा है, तो वह मुख्य रूप से इस बात की तारीफ करता दिखता है कि future को कभी भी drop किया जा सकता है। threads में साफ कारणों से ऐसा कुछ नहीं किया जा सकता, और तकनीकी रूप से संभव हो भी तो यह बेहद unsafe होगा। लेकिन इस क्षमता की बहुत बड़ी कीमत है। io-uring जैसे completion-आधारित executor के साथ stack-आधारित arrays का इस्तेमाल नहीं किया जा सकता, या sub-tasks को किसी दूसरे executor thread पर नहीं चलाया जा सकता; साथ ही सूक्ष्म pitfalls और reliability issues भी पैदा होते हैं, जो synchronous Rust लिखते समय बहुत अप्रिय surprise बन जाते हैं
    https://smallcultfollowing.com/babysteps/blog/2022/06/13/asy...
    task cancellation मूल रूप से cooperative cancellation ही होना चाहिए; non-cooperative cancellation ऊपर से सुविधाजनक लगता है, लेकिन अंदर गहरी समस्याओं वाला गलत feature जैसा है
    साथ ही async/await की composability की तारीफ करना भी अजीब है। proper effect system के बिना मौजूदा Rust में, इसकी infectious nature की वजह से यह composability से काफी दूर है। उदाहरण के लिए standard library के map method के साथ async closure इस्तेमाल करके देखें, या standard io::Read/Write traits इस्तेमाल करके देखें

    • Rust में fiber को user-space cooperative concurrency abstraction के रूप में डालने के लिए कई design decisions force करने पड़ेंगे
      जैसे stack को spaghetti stack के रूप में implement करना है, process-level memory mapping library की जरूरत रखनी है, या fixed-size stack तक सीमित रखना है
      इन तीनों तरीकों में अलग ABI वाली language code के साथ interact करते समय समस्याएँ आती हैं। उदाहरण के लिए, अगर किसी fiber से C code call किया गया और वह C code किसी दूसरे fiber को resume करने की कोशिश करे, तो मामला काफी जटिल हो सकता है
      async/await के फायदों में से एक खुद await keyword है। explicit wait points की वजह से concurrency program के interactions के बारे में सच में reason किया जा सकता है
      yield करने वाला fiber concurrency की दुनिया के goto जैसा है। कोई method call करते समय आपको पता नहीं होता कि side effect के रूप में execution रुक जाएगा या नहीं, या resume होने पर दुनिया की state बदल चुकी होगी या नहीं। बाहरी दुनिया से संपर्क करते समय defensive तरीके से लिखना पड़ता है, इसलिए fibers उन tasks के लिए ज्यादा उपयुक्त हैं जो isolated रूप से चलते हैं और completion के जरिए communicate करते हैं
      green threads, fibers और coroutines यहाँ एक ही समस्या share करते हैं। user-space cooperative concurrency, concurrency के कठिन हिस्से को हल करने से ज्यादा मेज पर रखे कागज इधर-उधर करने जैसा है। Rust async/await ज्यादा explicit है, इसलिए वह उन side effects को नहीं छिपाता जिन्हें दूसरे mechanisms छिपा देते हैं
    • मुझे समझ नहीं आता कि fibers cancellation समस्या को कैसे हल करते हैं। क्या यह लगभग equivalent नहीं है?
      fiber-based code में चलते हुए thread को दिमाग में track करना पड़ता है, इसलिए उसे follow करना मुश्किल लगता है। कम से कम मेरे लिए, पूरा होने वाली value को track करना कहीं आसान है
    • stack वाले fibers low-level code के लिए खास अच्छे नहीं हैं
      C++ committee के लिए Gor Nishanov की review http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p136... देखें। इसे https://devblogs.microsoft.com/oldnewthing/20191011-00/?p=10... में भी link किया गया है। summary भी साफ है: DO NOT USE FIBERS!
    • Ruby है, इसलिए thread/GVL situation Rust से अलग है, लेकिन शायद इसी तरह की बात कही जा रही है
      https://m.youtube.com/watch?v=qKQcUDEo-ZI
      मुझे लगता है यह काफी अच्छे से दिखाता है कि async/await infectious और awkward है, और कम से कम Ruby implementation में fiber कहीं बेहतर paradigm है
    • math problems में cooperative cancellation काफी परेशान कर सकता है
      कोई optimization algorithm root-finding problem को call कर सकता है, और वह ODE integrator को call कर सकता है, इस तरह nesting हो सकती है, और किसी भी stage में बहुत लंबा समय लग सकता है। cancellation token हर जगह pass करना पड़ेगा, लेकिन numerical computation frameworks आम तौर पर इसे support नहीं करते
      हर algorithm पर iteration count limit लगाई जा सकती है और लगानी भी चाहिए, लेकिन nested algorithms में बस इतना guarantee होता है कि इस साल के भीतर रुक जाएगा; 5 सेकंड में रुकने की guarantee देना मुश्किल है
      ऐसी problems में मैं guarantee दे सकता हूँ कि मेरा काम सिर्फ大量 mathematical computation, allocation और उससे होने वाले page faults, कोई I/O नहीं, और उन log strings को standard library Queue object में लिखना है जिन्हें main thread handle करेगा और जिसे cancel नहीं किया जाएगा। जो दूसरी functionality चाहिए, उसे भी Queue के जरिए main thread को वापस भेजा जा सकता है
      21वीं सदी में यह समस्या बिना cancellation token को हर जगह जबरन pass कराए, और बिना token check किए लंबे समय तक चलने वाले code को defensive ढंग से लिखने पर मजबूर किए, हल हो जानी चाहिए—ऐसा लगता है
  • async/await पर एक और discussion में लोग async/await को समझ नहीं पाते, यह कल्पना नहीं कर पाते कि single thread पर concurrency mechanism की जरूरत क्यों होगी, और मान लेते हैं कि किसी को इसकी जरूरत नहीं है
    UI programming, GPU से communication, runtime-to-runtime communication इसके अच्छे उदाहरण हैं, और भी होंगे
    threads—चाहे green threads हों या नहीं—इन cases के लिए fit नहीं हैं, लेकिन async/await fit है

    • GUI में threads आसानी से इस्तेमाल किए जा सकते हैं, और मैंने पहले कई GUI apps लिखे हैं जिन्होंने threads का काफी प्रभावी इस्तेमाल किया
    • single thread पर कई tasks को explicitly manage कर पाना निश्चित रूप से महत्वपूर्ण है
      अगर वही binary बनाते हुए language users के लिए कम झंझट वाली दूसरी language feature implementation संभव हो, तो उस पर भी बात की जा सकती है
  • Rust async/await का एक मुख्य फायदा यह है कि यह threads या dynamic memory न होने की स्थिति में भी काम कर सकता है
    माइक्रोकंट्रोलर पर interrupt किसी buffer में आने वाले I2C डेटा को पढ़ने तक इंतज़ार करने वाला संक्षिप्त code लिखने के लिए भी यह काफी उपयोगी हो सकता है। यह एक higher-level abstraction है, जो नीचे के runtime के साथ बहुत अधिक interaction उजागर किए बिना concurrency इस्तेमाल करने देता है
    जिन भी बड़े software पर मैंने काम किया है, उन सभी ने किसी न किसी रूप में ऐसी चीज़ implement की थी। आधुनिक C++ coroutine concept न होने वाले code में भी Apple Grand Central Dispatch, Intel Threading Building Blocks वगैरह इस्तेमाल किए गए थे। वरना business logic I/O पर बहुत inefficient तरीके से block हो जाता है, या threads की संख्या इतनी ज़्यादा हो जाती है कि development और debugging नरक बन जाते हैं, या code नीचे के runtime implementation details से भर जाता है—या फिर इन तीनों का मिश्रण होता है
    अगर आप language में मौजूद abstraction या library के existing abstraction का इस्तेमाल नहीं करते, तो आखिरकार आपको खुद बनाना पड़ता है, और यह कठिन होता है तथा व्यापक रूप से इस्तेमाल होने वाली चीज़ों की तुलना में कुल मिलाकर inferior होने की संभावना ज़्यादा होती है। पहले मैंने C++ के लिए खुद ऐसा बनाया भी था: https://github.com/goto-opensource/asyncly

  • लगता है लेखक दो चीज़ों को मिला रहे हैं
    एक है user-space threads/green threads, और दूसरी है structured concurrency
    पहला async/await का फायदा है, लेकिन इसका exclusive फायदा नहीं है। Go या Java Loom जैसे उदाहरण हैं, जहाँ function color problem के बिना यह संभव है
    दूसरा OS threads और green threads, दोनों से implement किया जा सकता है। Java का Structured Concurrency JEP देखें
    https://openjdk.org/jeps/462