2 पॉइंट द्वारा GN⁺ 2023-09-09 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Rust का async/await दसियों हज़ार connections संभालने वाली बड़े पैमाने की concurrency को लक्ष्य करता है, लेकिन यह low-level control और static lifetime verification जैसे Rust के लक्ष्यों से टकराता है, जिससे सामान्य Rust से अलग development experience बनता है
  • Threads और channels बहुत से software के लिए पर्याप्त हैं, लेकिन C10K जैसे scale पर per-thread-per-connection तरीके का बोझ बढ़ जाता है, इसलिए user-space tasks और runtime scheduling की ज़रूरत पड़ती है
  • async Rust में data को Send के रूप में move होना पड़ता है या 'static references के रूप में संभालना पड़ता है, और async की infectious प्रकृति के कारण ये constraints पूरे codebase में बार-बार सामने आते हैं
  • Arc compile की समस्या हल कर दे, तब भी objects और resources की lifetimes को धुंधला बना देता है; इसके बाद recursive async, future और task के अंतर, तथा blocking calls के runtime threads को रोक देने जैसे pitfalls आते रहते हैं
  • Haskell या Go में “async code” सामान्य code की तरह काम करता है और runtime व GC अंतर को छिपा देते हैं, इसलिए इस तरह की programming में Rust का explicit control हमेशा शुद्ध लाभ की तरह काम न करे

Concurrency और parallelism की ज़रूरत क्यों है

  • तेज़ programs के साथ दो requirements साथ-साथ जुड़ी होती हैं
    • कई CPU cores का उपयोग करके पूरी machine का इस्तेमाल करना होता है
    • internet message भेजने या file खोलने जैसे धीमे operations का इंतज़ार करते समय दूसरे काम जारी रखने होते हैं
  • Parallelism कई CPUs पर code को एक साथ चलाने की समस्या है
  • Concurrency समस्या को स्वतंत्र हिस्सों में बाँटने का तरीका है
  • दोनों समान नहीं हैं, लेकिन program को concurrent टुकड़ों में बाँटने पर वे टुकड़े parallel रूप से चल सकते हैं और cores को लगातार busy रख सकते हैं

Processes, threads, channels

  • concurrent system बनाने का एक सरल तरीका code को कई processes में बाँटना है
    • operating system scheduler runnable processes के time slices को उपलब्ध CPU cores पर चलाता है
    • shell commands को pipes से जोड़ने पर भी यही model इस्तेमाल होता है
  • process approach में inter-process communication की cost बड़ी होती है
    • कई implementations में data को OS memory में copy करके फिर वापस लाना पड़ता है
    • shared memory से cost घटाई जा सकती है, लेकिन OS द्वारा processes को एक-दूसरे से isolate करने का फायदा कमज़ोर हो जाता है
  • Threads वही memory share करके इस overhead से बचते हैं, लेकिन mutex, condition variable, semaphore जैसे synchronization tools का गलत उपयोग data races और deadlocks पैदा कर सकता है
  • Tony Hoare का Communicating Sequential Processes model threads को queues या channels से जोड़ता है
    • threads memory share नहीं करते, इसलिए process जैसी isolation मिलती है
    • हर thread का input और output channel के रूप में दिखता है, जिससे reasoning और debugging आसान होती है
    • channel खुद synchronization की भूमिका निभाता है; खाली होने पर receiver इंतज़ार करता है और भर जाने पर sender इंतज़ार करता है
  • Rust standard library में std::sync::mpsc::sync_channel है
  • बहुत से software के लिए threads और channels, तथा CPU-intensive loops को parallelize करने वाले Rayon जैसे tools का combination पर्याप्त होता है

User-space concurrency और Rust async

  • C10K problem, जैसे ऐसा web server जहां दसियों हज़ार concurrent users connected हों, में एक thread को एक connection से जोड़ने का तरीका अपनी सीमा पर पहुँच जाता है
    • Linux में हर thread के पास 4kB control block होता है, और thread switching के लिए operating system scheduler में जाने वाला context switch चाहिए होता है
  • बड़े पैमाने की concurrency के लिए कुछ languages tasks को user space में बनाती और manage करती हैं
    • runtime tasks को OS thread pool पर schedule करता है
    • आम तौर पर pool को इस तरह configure किया जाता है कि हर CPU core पर एक thread हो, ताकि parallelism maximize हो
    • इस तरीके को green thread, lightweight thread, lightweight process, fiber, coroutine आदि कहा जाता है
  • Rust C# या Node.js में दिखने वाला async/await model इस्तेमाल करता है
    • async fn सीधे value return नहीं करता; वह future या promise return करता है, जिसका result .await से मिलता है
  • Rust future cooperative scheduling और stackless design की वजह से बहुत छोटा और तेज़ होता है
  • Rust future abstraction देते हुए भी programmer को low-level control का वादा करना चाहता है
    • वह सभी objects और references की lifetimes को compile time पर statically verify करना चाहता है
    • future code और उस code द्वारा reference किए गए data को हज़ारों टुकड़ों में बाँटता है, और उन्हें ऐसी conditions के आधार पर किसी भी समय किसी भी thread पर चलने देता है जो execution शुरू होने के बाद ही पता चलती हैं
    • client data पढ़ने वाला future तभी चलना चाहिए जब उस socket पर पढ़ने के लिए data हो, लेकिन lifetime annotation उस समय के बारे में नहीं बताती
  • Rust future runtime को language में built-in नहीं करता और इसे Tokio जैसी libraries पर छोड़ता है
    • users को अपने environment के लिए उपयुक्त alternatives चुनने की freedom मिलती है
    • लेकिन अगर हम Tokio के language में built-in होने वाली दुनिया की कल्पना करें, तब भी वही rules लागू होंगे, इसलिए इस argument में यह secondary detail है

Send, 'static, Arc से बनने वाला दबाव

  • compiler को convince करने के लिए data को Send के रूप में marked होकर move होना चाहिए, या 'static lifetime वाले references के जरिए pass होना चाहिए
  • async code में कई tasks का common state share करना आम है, इसलिए बिना copying data move करने का तरीका अक्सर fit नहीं बैठता
  • references भी कठिन हैं, और future की lifetime को “हमेशा के लिए” से छोटा restrict करने वाला thread::scope जैसा counterpart नहीं है
  • async infectious है, इसलिए async function को call करने वाला function भी async होना चाहिए
    • इसलिए ऐसी lifetime और mobility problems को कुछ functions में नहीं, बल्कि लगातार solve करना पड़ता है
    • runtime में future completion का block_on से इंतज़ार करके chain को तोड़ा जा सकता है, लेकिन यह तरीका composable नहीं है और nested होने पर runtime panic कर सकता है
  • Arc dynamic lifetimes को कई threads में संभालने का tool है, और borrow check पास कराकर code compile करवा देता है
  • लेकिन Arc का व्यापक उपयोग objects और resources की lifetimes को धुंधला कर देता है
    • memory, files, sockets जैसे resources कब release होंगे, यह स्पष्ट नहीं रहता
    • actual GC द्वारा मिलने वाले allocation throughput, low fragmentation, cycle leak avoidance जैसे फायदे बिना पाए GC जैसी ही costs झेलनी पड़ती हैं

async Rust के अतिरिक्त pitfalls

  • Rust coroutine stackless होता है, इसलिए compiler हर coroutine को .await points तक आगे बढ़ने वाली state machine में बदल देता है
    • recursive async function recursively defined type बन जाता है
    • जो user बस खुद को call करना चाहता है, उसे manually boxing करनी पड़ती है या async-recursion जैसा crate इस्तेमाल करना पड़ता है
  • future await होने से पहले कुछ नहीं करता
  • task runtime के thread pool में काम शुरू करता है और completion दिखाने वाला future return करता है
  • future के अंदर blocking code call करने से रोकने वाला कोई mechanism नहीं है
    • यह भी नहीं रोकता कि ऐसी call जिस runtime thread पर चल रही है, उसे block कर दे
    • यह async इस्तेमाल करने के core purpose से टकराता है

सामान्य Rust, Haskell, Go के अंतर

  • async Rust का स्वाद “normal” Rust से काफी अलग है
    • इसमें ज़्यादा pitfalls हैं
    • इसे समझना और सिखाना अधिक कठिन है
  • users दो choices के बीच फँसते हैं
    • abstraction असल में कैसे काम करता है, इसे गहराई से समझकर complex code लिखें
    • Arc, Pin, 'static जैसे elements को code में जगह-जगह डालें और उम्मीद करें कि सब ठीक रहेगा
  • experienced developers की teams भी नए project में Rust इस्तेमाल करने की कोशिश करते हुए इन details में अटक सकती हैं
  • Haskell या Go में “async code” सामान्य code ही होता है
    • दोनों languages blocking और non-blocking code के अंतर को मोटे runtime के पीछे छिपा देती हैं
    • lifetime problems को garbage collection पर छोड़ देती हैं
  • इस तरह के बड़े पैमाने के concurrent user-space software में runtime और GC द्वारा अंतर छिपाने का तरीका शुद्ध लाभ की तरह काम करता है
  • Rust बड़े पैमाने के concurrent user-space software के लिए अच्छा tool न हो सकता है, और बेहतर हो सकता है कि इसे ऐसे projects में इस्तेमाल किया जाए जहाँ ऐसी requirements न हों

1 टिप्पणियां

 
GN⁺ 2023-09-09
Hacker News की राय
  • मैं Rust में एक हाई-परफॉर्मेंस metaverse client लिख रहा हूँ, और अभी इसका आकार करीब 40,000 लाइनों का है
    डेमो वीडियो https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz पर है
    एक ठीक-ठाक metaverse को यूज़र द्वारा बनाए गए content को लगभग real-time में प्रोसेस करना पड़ता है, इसलिए समान गेम्स की तुलना में 2–3 गुना VRAM चाहिए, और server से assets लोड करने के लिए सैकड़ों Mbps bandwidth, कई CPUs, और rendering व GPU uploads को साथ-साथ चलाने के लिए Vulkan चाहिए
    यह “web scale” concurrency जैसा नहीं है, जहाँ एक ही address space में छोटे-छोटे servers अलग-अलग चलते हैं; बल्कि इसमें high-priority render thread, network event update thread, asset loading/decompression thread, और moving objects, LOD, cache cleanup आदि संभालने वाले कई threads साथ मिलकर चलते हैं
    Rust में मैं constants के अलावा global state के बिना काफी locking इस्तेमाल करता हूँ, channels जहाँ फिट बैठते हैं वहाँ इस्तेमाल करता हूँ, और मुख्य object tree को single ownership के साथ update thread ज़्यादातर संभालता है। graphics object connections को Arc reference counting से manage किया जाता है, और Rend3/WGPU/Vulkan के जरिए meshes और textures को GPU पर upload किया जाता है
    अगर यह C++ में किया होता तो लगातार crashes से लड़ना पड़ता, लेकिन Rust में memory-related crashes साल में लगभग एक बार होते हैं, और वे भी आम तौर पर किसी और के unsafe code की वजह से। अपने code में मैंने unsafe को ban कर रखा है, और compile करना मुश्किल है, लेकिन एक बार हो जाए तो यह “बस चल जाता है”, इसलिए मुझे लगता है कि यह concurrency debugging से कहीं बेहतर है
    शिकायतें भी हैं। Rust data races के खिलाफ मजबूत है, लेकिन deadlocks नहीं रोकता, इसलिए call paths के साथ lock order track करने वाला static analyzer चाहिए। async compute-heavy work और अलग-अलग priority वाले threads के लिए फिट नहीं है, फिर भी dependencies के जरिए बार-बार अंदर घुस आता है। single ownership के साथ back-references वाला आम pattern Rc और Weak के बिना बहुत कठिन है, और trait system भी complex है, जिससे asset processing के उन हिस्सों में duplicate code बनता है जहाँ object-oriented तरीका ज्यादा natural है
    graphics core crates भी अभी पूरी तरह mature नहीं हैं। “Rust में 5 games और 50 game engines हैं” — यह language की समस्या नहीं, ecosystem की समस्या है, और https://gamedev.rs/ से तुलना करने पर भी Rust में serious game development अभी कम दिखती है। अगर schedule वाला professional game development करना हो, तो Rust game ecosystem अभी तैयार नहीं है; मुझे लगता है इसे तैयार होने के लिए करीब 5 लोगों का लगभग 1 साल और काम चाहिए

    • पिछले 3 साल से Rust में robot simulator बना रहा हूँ, और अनुभव लगभग यही है। 3 साल में असली runtime bugs करीब 5 ही थे, और Rust व async में समस्याएँ होने के बावजूद कुल मिलाकर फायदे कहीं ज्यादा हैं
    • lock order follow करके संभावित deadlocks खोजने का idea अच्छा लगता है
      Linux के lockdep की तरह यह analyze किया जा सकता है कि एक lock पकड़े होने पर कौन-सा दूसरा lock पकड़ा जाता है, और असल में रुकने से पहले ही risky combinations बता सकता है। complex locks में “यह lock class हमेशा address order में ली जाती है” जैसी annotations चाहिए होंगी, लेकिन implementation संभव लगती है
    • MMO में लगभग यही काम Java में कर रहा हूँ और JDK इसे बहुत आसान बना देता है। network से models बनाइए, objects को concurrent queue के जरिए UI thread पर ले जाइए; यह काफी boring हद तक सरल है, फिर भी fast है
    • Rust में race conditions नहीं होतीं, ऐसा नहीं है; इसमें data races नहीं होतीं
      data access के बाहर अभी भी race conditions हो सकती हैं: https://news.ycombinator.com/item?id=23599598
    • priority की समस्या अपेक्षाकृत आसानी से हल की जा सकती है
      कई thread pools बनाकर futures को सही जगह route करें, या अपना event loop लिखकर अलग-अलग priority वाली कई event queues से items लें। दूसरा तरीका, अगर task execution time bounded हो, तो CPU 100% स्थिति में भी low-priority tasks को आगे बढ़ाते हुए high-priority tasks को soft real-time guarantee दे सकता है
  • async Rust को लेकर स्थिति थोड़ी अजीब है
    अगर आप Arc, RwLock और shared state बहुत ज्यादा इस्तेमाल करते हैं तो code गंदा हो जाता है, और खासकर जब 'static हर जगह फैलने लगता है, तो यह color functions की तरह सब कुछ infect कर देता है — यह बात सही है। पहले मैं Arc जोड़कर lifetimes/borrowing को clever तरीके से handle करने की कोशिश करता था और सब mess बन जाता था
    लेकिन Rust में channels भी हैं। अभी जो code मैं लिखता हूँ, उसमें ज्यादातर कुछ tasks channels को service करते हैं, incoming messages देखते हैं और जरूरत पड़ने पर दूसरे task को भेजने वाला message सही channel में डाल देते हैं। objects share नहीं किए जाते। अगर कई tasks को किसी बड़े object की जरूरत हो, तो उसे उस task में रखा जाता है जो relevant query results messages के रूप में भेजता है, या हर task message flow में अपनी copy बना लेता है
    फिर भी Arc कैसे इस्तेमाल करें और lifetimes कैसे handle करें, इस पर बहुत सारे लेख हैं। अगर आप async runtime implement कर रहे हैं तो इसकी जरूरत होगी, लेकिन average library user को इस पर इतना focus क्यों करना चाहिए, यह मुझे समझ नहीं आता

    • आलोचना थोड़ी अजीब लगती है। async का मतलब अपने आप multi-threading नहीं होता, और अगर एक ही thread के अंदर async है तो sharing नहीं होती, इसलिए shared चीजों पर हर तरह के magical keywords लगाने की जरूरत भी नहीं है
      threads के बीच जाते समय shared state बहुत रखने के बजाय channels से signals भेजे जाते हैं। अगर कोई सचमुच जरूरी global state हो, तो Arc/RwLock जैसे exclusive access mechanisms को wrap करने वाला छोटा struct बना लें, ताकि caller को यह साधारण function call जैसा दिखे
      Send+Sync को लेकर चिंता भी अच्छी तरह समझ नहीं आती। मेरे अनुभव में ज्यादातर चीजें आसानी से Send+Sync होती हैं, और जो नहीं होतीं वे या तो वैसी होनी नहीं चाहिए या हो ही नहीं सकतीं। कभी-कभी details सोचे बिना code लिखना अच्छा लगता है, लेकिन अगर efficient concurrency और parallelism चाहिए, तो microseconds और throughput मायने रखते हैं, और तब असली computer code ठीक से लिखना पड़ता है
    • message passing paradigm सचमुच अच्छा है, और Erlang जैसी languages ने दिखाया है कि distributed systems में यह शानदार choice हो सकती है
      लेकिन इस तरह की coding async JavaScript से काफी अलग है, जो synchronous code पर green threads लगाने जैसी लगती है। लोग familiar तरीके से code लिखना चाहते हैं, इसलिए शायद Rust में Arc और RwLock वाले रास्ते पर चले जाते हैं
    • Smalltalk और असली object-oriented programming का सपना अभी भी जिंदा है
    • कॉलेज में एक professor से यह सलाह सीखी थी, और यह सच में बहुत काम आई
      समस्या को tasks के बीच बहने वाले data के रूप में structure करना, उन्हें queues से जोड़ना और shared state से बचना — चाहे कोई भी language हो, multi-threading संभालने का बेहतर तरीका है
    • जैसा एक समझदार programmer ने कहा था, “memory share करके communication मत करो; communication के जरिए memory share करो”
  • async असल में कहीं ज़्यादा कठिन Rust है, और जिन projects को सचमुच इसकी ज़रूरत होती है वे शायद 1% ही होंगे; फिर भी इसे लगभग सब पर थोपा गया-सा लगता है, यह अफ़सोस की बात है
    हालांकि उस 1% में यह वाकई शानदार है। linkerd या nginx जैसी services, जिनका core बड़ी संख्या में network calls संभालना है; games में बहुत बड़ी संख्या में हल्के tasks चलाने के मामले; या embedded में cooperative concurrency की ज़रूरत हो, तो async Rust एक मजबूत हथियार बन जाता है
    ज़्यादातर system/application-level code को asynchronous I/O की ज़रूरत नहीं होती। REST apps के लिए thread pool काफी है, और अगर async चाहिए भी तो network जैसे छोटे हिस्से तक सीमित रखना और बाकी को threads व channels से जोड़ने वाला mixed model आम तौर पर सही होता है
    Rust community ने async को हर जगह बहुत अंधाधुंध इस्तेमाल किया, जिससे बेहतर user experience वाला blocking I/O Rust ecosystem में second-class citizen बन गया। Web frameworks में Axum, Warp जैसे अच्छे design वाले कई async frameworks हैं, लेकिन blocking side में tiny_http, rouille, astra जैसे options कहीं ज़्यादा सीमित हैं

    • मुख्य बात यह है कि Rust ने coroutines को गलत तरह से implement किया
      stackless coroutines चुनने से async/await और colored functions की समस्या पैदा हुई, और लेख में बताई गई friction आई। Go stackful coroutines इस्तेमाल करता है, इसलिए उसमें यह समस्या नहीं है
      Rust ने भी शुरुआत में stackful coroutines पर विचार किया था, लेकिन coroutine preemption runtime की ज़रूरत और लागत ज़्यादा मानकर stackless model अपनाया। लेकिन ज़्यादातर लोग runtime-less async Rust नहीं, बल्कि Tokio इस्तेमाल करते हैं, और Tokio असल में वह लगभग सब कुछ करता है जो वह runtime करता जिससे बचना था
      इसलिए अधिकांश async Rust users को दोनों तरफ़ की खराबियाँ मिलती हैं। Embedded side में कुछ लोग बहुत पतले runtime के साथ async Rust इस्तेमाल करते हैं, लेकिन उनकी संख्या भी कम है और वे भी पूरी तरह आश्वस्त नहीं हैं
    • मैंने देखा कि मेरे program में Tokio फिर से dependency बनकर घुस आया। मैं उसे सीधे इस्तेमाल भी नहीं करता, लेकिन किसी crate का वह function जिसे मैं इस्तेमाल नहीं करता reqwest लाता है, वह h2 लाता है, और फिर वह tokio लाता है
    • अगर platform virtual threads support करता है, तो मुझे आश्चर्य है कि async इस्तेमाल करने की वजह बचती भी है या नहीं
      Java user के तौर पर, मैं पूरे asynchronous paradigm को छोड़कर code को virtual threads पर आधारित blocking model में फिर से लिखने की कोशिश कर रहा हूँ, जहाँ blocking ठीक है
  • async इतने सारे crates में फैल चुका है कि पूरा program async बन जाता है, या कम से कम बहुत से कामों के लिए Tokio पर निर्भर हो जाता है
    Web server चाहिए तो मानो async + tokio लो वरना निकलो, और SQL connector भी अगर async नहीं चाहिए तो खुद लिखो—ऐसा माहौल है। हर कोई async से आने वाली समस्याओं को अलग-अलग तरीके से solve कर रहा है, और async closures जैसी चीज़ें compiler के लिए नर्क का दरवाज़ा खोलती महसूस होती हैं
    Rust खुद और compiler problems solve करने में मदद करते हैं, यह अच्छी बात है, लेकिन ecosystem का “async नहीं तो खुद बनाओ” के करीब होना पर्याप्त नहीं है

    • standard library या futures crate में बेहतर asynchronous primitives होते तो दर्द बहुत कम हो सकता था
      executor को implement करने वाली traits, या synchronous code से asynchronous code चलाने के लिए basic blocking executor जैसी चीज़ों की ज़रूरत है। अभी multiple async runtimes support करने वाली library बनाना ही इतना कठिन है कि अंत में लोग केवल Tokio support करते हैं, या बहुत हुआ तो async-std जोड़ देते हैं
  • मैं async Rust expert नहीं हूँ, लेकिन इस महीने synchronous Rust की कुछ हज़ार lines लिखते हुए मुझे लगा कि जब rustc किसी approach को कठिन बनाता है, तो आम तौर पर उसके पीछे वजह होती है और वैसा ही result बेहतर तरीके से पाने का कोई रास्ता मौजूद होता है
    अगर आप language सीख रहे हैं, तो पहले साधारण synchronous code, loops और conditionals, और borrowing rules से परिचित होने की सलाह दूँगा। async सिर्फ implementation में ही नहीं, बल्कि “asynchronous क्या है और user को कैसा दिखना चाहिए” जैसे philosophical level पर भी अभी काफी evolve हो रहा है
    compiler traits पर बहुत निर्भर करता है, लेकिन traits के async को handle करने वाले features stable नहीं हुए हैं। उदाहरण के लिए https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i... जैसा काम है
    अगर traits की async capabilities stable नहीं हुई हैं, तो Rust के async code को अभी सुंदर नहीं है कहकर attack करना आखिरकार उस किताब के शुरुआती draft की आलोचना करने जैसा है जो अंततः पूरी होगी

    • मुझे जानना है कि “अच्छा asynchronous API design” क्या होता है। अगर पूरी तरह async-centric जाते हुए scalable, maintainable और समझने में आसान server design करना हो, तो वह कैसा दिखेगा
      यह भी चिंता है कि async को पूरे codebase में फैलने से कैसे रोका जाए
      मौजूदा idea यह है कि I/O thread liburing या epoll के system events को “submit” और “handle” दो चरणों में बाँटकर दूसरे components को भेजे। उदाहरण के लिए, tcp-connection बनाने पर “write ready”, “read ready” जैसे async events subscribe किए जा सकते हैं, और write-ready event सामान्य mutex से भरे buffer से data निकालकर EPOLLOUT/io_uring_prep_writev से भेजता है
      threads के बीच event delivery के लिए LMAX Disruptor pattern का multi-producer, multi-consumer ring buffer इस्तेमाल किया जा सकता है। Application threads या thread pool अपने-अपने event loop रखते हैं और इस ring buffer को process करते हैं
      asynchronous event firing order को express करने वाली syntax पर भी काम चल रहा है, जो Bash pipeline जैसी दिखती है और statelines कहलाती है: initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3
    • अगर stable नहीं हुआ है तो production में भी इस्तेमाल नहीं करना चाहिए
    • Rust beginner मानकर लिखी गई टिप्पणियाँ मज़ेदार हैं। उल्टा हो सकता है कि लेखक का experience उनसे ज़्यादा हो
  • Arc की lifetime अज्ञात नहीं होती, बल्कि यह इस बात से तय होती है कि उसे कहाँ और कैसे रखा गया है
    इस लेख में जो disconnect है, वह शायद इस वजह से है कि लेखक Rust सीखकर भाषा के हिसाब से काम करने के बजाय, garbage collection जैसे पुराने सोच के model को Rust पर जबरन लागू करना चाहता है। नई भाषा सीखते समय यह आम trap है, लेकिन Rust में लोग खास तौर पर ज़्यादा बार इसमें अटकते हैं

    • उस अर्थ में देखें तो garbage collection system में object lifetime की भी “जब तक reference है” वाली निचली सीमा होती है
      लेकिन यह compile time पर object lifetime को statically सीमित करने की borrow checker की मंशा से लगभग उल्टा है
      असल में मेरा अनुभव इसके लगभग उलट रहा। C, C++, Rust में करीब 10 साल system programming करने के बाद, मौजूदा नौकरी में Haskell का काफी इस्तेमाल करने लगा, और यह समझना काफी eye-opening था कि बड़े language runtime और garbage collection कुछ problem domains में कोई राक्षस नहीं हैं
    • आलोचना का बड़ा हिस्सा ऐसा ही लगता है। मुझे लगा था यह लेख इस बारे में होगा कि async transformation, non-async code में compiler द्वारा की जा सकने वाली optimizations को कैसे बाधित करता है
      Weak से जूझने वाला हिस्सा complex ownership structure बनाने की कोशिश जैसा दिखता है, जो Rust में आम तौर पर आसान काम नहीं है। मैं weak smart pointers बहुत कम इस्तेमाल करता हूँ
      channels का लगभग ज़िक्र नहीं है, जबकि async code या async और sync code के बीच bridge बनाते समय program के अलग-अलग हिस्सों को communicate कराने का मुख्य tool वही हैं। Notify, semaphores जैसी signaling abstractions भी हैं
      mutex धीमा होता है और आसानी से bottleneck बन सकता है, और shared state जल्दी ही complex हो जाती है। यह बहुत पहले से ज्ञात बात है। समस्या शुरू से ही BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORROR जैसी structure में हो सकती है
      यह बात सही है कि async context में blocking code call करने से रोक नहीं सकते, लेकिन ज़रूरत हो तो tokio::spawn_blocking जैसी चीज़ों से इसे अपेक्षाकृत manage किया जा सकता है
    • Reference counting भी garbage collection का एक प्रकार है https://en.wikipedia.org/wiki/Garbage_collection_(computer_s...
      लेखक को शायद पता है कि Arc क्या है और कैसे काम करता है; मुख्य बात यह लगती है कि Rust async में sync code की तुलना में सामान्य RAII के बजाय Arc का इस्तेमाल बहुत ज़्यादा करना पड़ता है
      अगर program objects के 90% पर reference counting हो रही है, तो कई छोटे heap allocation/deallocation और atomic operation की cost चुकाने के बजाय tracing garbage collection इस्तेमाल करना बेहतर हो सकता है। Tokio tutorial के उदाहरण भी इसी दिशा की ओर इशारा करते हैं: https://tokio.rs/tokio/tutorial/shared-state
      मुझे जिज्ञासा है कि Rust में वास्तविक tracing garbage collection, HTTP server जैसी सामान्य async applications को अर्थपूर्ण रूप से तेज़ बना सकती है या नहीं: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
    • Arc की lifetime random नहीं, बल्कि statically unknown होती है
    • Rust का Arc move या borrow किया जा सकता है, और reference count को छुए बिना भी इस्तेमाल किया जा सकता है
      कई मामलों में यह implicit reference-counting languages के objects से कहीं सस्ता होता है
  • मुझे Rust पसंद है, लेकिन async एक mess है, और async code को sync code की तरह नहीं लिखा जा सकता
    मुझे यह यकीन बढ़ता जा रहा है कि दोनों को मिलाना बुरा विचार है, और शायद Go-style approach सही हो सकती है जिसमें सब कुछ synchronous रखा जाता है और केवल एक async channel primitive दिया जाता है
    अभी मैं Future implement करने वाली struct में synchronous methods call कराने के लिए logic wire कर रहा हूँ, और यह काफी दिलचस्प चुनौती है। zero-cost async abstraction को users के लिए कुछ हद तक आसान बनाया जा सकता है, लेकिन दर्द library developers को उठाना पड़ता है

    • आखिरी बात से सहमत नहीं हूँ। async end users के लिए भी निश्चित रूप से दर्दनाक है; ऐसा लगता है जैसे Rust की core features—lifetimes और explicit types—के बिना कोई अलग भाषा इस्तेमाल कर रहे हों, जिस पर ढेर सारा Pin छिड़क दिया गया हो
      scoped fibers चलाए नहीं जा सकते, इसलिए आखिरकार बहुत सारे Arc लगाने पड़ते हैं; Pin को unsafe के बिना इस्तेमाल करना मुश्किल है; और async function में बहुत छोटा बदलाव पूरे codebase के futures को !Send बना सकता है
    • library developers के पास users की तुलना में complexity संभालने की अधिक क्षमता होती है। ऐसी चीज़ें skilled developers को देना, जो basic infrastructure बना रहे हैं, सही दिशा है
    • मैंने Rust के लिए एक wasm VM का उदाहरण देखा था, जो transparent M:N scheduling जैसी चीज़ देता दिखता है, और उस तरीके से async की ज़्यादातर मुश्किलें हल हो सकती हैं। देखना होगा यह कैसे विकसित होता है
  • Async Everything एक खराब भाषा है
    async/await JavaScript में सही blocking threads न होने की समस्या को ठीक करने की एक भयानक idea था, और अब इसे हर भाषा में जोड़ दिया जा रहा है। यह भाषा और library ecosystem को दो हिस्सों में बांटकर लंबे समय तक दर्द पैदा करेगा
    JavaScript के बाहर जिसने भी multi-threading की है, वह जानता है कि actors या communicating sequential processes multi-threading के लिए सबसे अच्छे तरीके हैं
    Joe Armstrong के पेपर में भी बताया गया है कि multi-threaded program को समझने का इकलौता तरीका यह है कि हर thread के लिए सख्ती से sequential code लिखा जाए, और कई threads के code को एक ही जगह मिलाया न जाए। समस्या की एक वास्तविक concurrent activity को programming language के एक concurrent process से बिल्कुल मेल खाना चाहिए, तभी conceptual gap न्यूनतम रहता है: https://erlang.org/download/armstrong_thesis_2003.pdf
    Java के Project Loom को implement करने वाले Ron Pressler की async/await पर आलोचना भी अच्छी है: https://www.youtube.com/watch?v=oNnITaBseYQ

    • JavaScript से नफरत करना मजेदार है, लेकिन Ryan Dahl की Node.js को पहली बार पेश करने वाली talk को दोबारा देखना दिलचस्प है: https://www.youtube.com/watch?v=EeYvFl7li9E
      JavaScript को लेकर वह खुद काफी ambivalent थे, और मुख्य लक्ष्य epoll() I/O event loop को ऐसे abstraction से संभालना था जिससे आंखों में उंगली डालने का मन न करे। उससे पहले उन्होंने कई दूसरे तरीके भी आजमाए थे
    • async/await असल में JavaScript से नहीं, C# से शुरू हुआ था
      C# के Anders Hejlsberg ने TypeScript भी बनाया, और TypeScript के class, arrow function, async/await जैसे features आखिरकार ES6+ में शामिल हुए
      single-threaded event loop वाले JS/TS में यह एक शानदार समाधान था, ऐसा मुझे लगता है। लेकिन भाषा जितनी low-level होती है, abstraction के रूप में यह उतना ही खराब होता जाता है, इसलिए यहां की गई async Rust की आलोचना अधिकतर सही है
  • लेख async Rust की जटिलता और कठिनाई को अच्छी तरह समझाता है, लेकिन यह भी महत्वपूर्ण है कि Rust की मुख्य philosophies में से एक performance से समझौता किए बिना memory safety है
    Rust के asynchronous patterns, खासकर compiler से data safety guarantee करवाने का तरीका, इस philosophy को अच्छी तरह दिखाते हैं। जटिलता है, लेकिन इसकी कीमत एक सुरक्षित concurrency model के रूप में है, जो developers को data और execution flow के बारे में गहराई से सोचने पर मजबूर करता है
    Rust हर बड़े concurrent user-space application के लिए सही जवाब नहीं हो सकता, लेकिन जिन systems में robustness और safety सर्वोच्च प्राथमिकता हैं, वहां यह trade-off जायज हो सकता है। ecosystem के विकसित होने के साथ ऐसी abstractions और libraries भी और आने की संभावना है जो इस दर्द को कम करेंगी

  • मैं async आधारित lock-free Rust काफी लिख रहा हूं। मुख्य समस्या यह है कि Tokio future 'static होता है, और यह Rust ecosystem में गहराई तक जमी एक design mistake—यानी memory leak सुरक्षित है—वाले फैसले से आता है
    इसकी वजह से यह statically guarantee नहीं किया जा सकता कि future सही से clean up होगा। जब कोई async task बनाया जाता है और कोई std::mem::forget से future को भुला देता है, तो borrow checker यह नहीं जान सकता कि उस future ने transitively जो references पास किए थे वे अभी भी alive हैं
    हर जगह Arc छिड़कने के बजाय मैं यह unsafe crate इस्तेमाल करता हूं: https://docs.rs/async-scoped/latest/async_scoped/
    इतना करने से C++ में बनने वाले bugs के 99% पकड़े जाते हैं, इसलिए यह reasonable trade-off है। safe तरीके से non-'static future implement करने का काम भी चल रहा है, उम्मीद है सफल होगा
    एक और बड़ी समस्या यह है कि async trait फिलहाल boxed future मांगता है, जिससे हर function call boundary पर malloc/free जुड़ जाता है, लेकिन यह इस साल के fix roadmap में है
    “बस channel इस्तेमाल करो” वाली सलाह भी बड़े codebase में control flow को हर तरफ बिखेर देती है। channels modern GOTO जैसे लगते हैं, और मैं भी उनका इस्तेमाल करता हूं, लेकिन जब केवल कुछ काम parallel में चलाकर उनके complete होने का इंतजार करना हो, तब उनका ज्यादा उपयोग नहीं करता

    • महत्वपूर्ण फर्क यह है कि Tokio future खुद 'static नहीं होता, बल्कि runtime की concurrency का लाभ उठाते हुए spawn किए जा सकने वाले future सिर्फ 'static होते हैं
      future को poll() किए जाने के लिए Pin होना चाहिए, और Pin किया गया T: !Unpin आखिरकार Drop call करना चाहिए: https://doc.rust-lang.org/std/pin/#drop-guarantee
      compiler के async feature से बने future में यह property होती है, और manual future में भी PhantomPinned डाला जा सकता है। इसकी वजह से poll() होने के बाद mem::forget वाली शरारत को undefined behavior माना जा सकता है, और intrusive/self-referential future libraries भी संभव हो जाती हैं: https://docs.rs/futures-intrusive/latest/futures_intrusive/
      future Arc/Rc के कारण जीवित रहकर leak हो सकता है, लेकिन library developer के नजरिये से इसे normal use से reasonable तरीके से अलग करना संभव नहीं, या इसकी ज्यादा परवाह करने की जरूरत नहीं होती
    • अगर memory leak के safe होने को design mistake माना जाए, तो उत्सुकता है कि आप internal mutability हटाना पसंद करेंगे, या Rc हटाना, या फिर infectious unsafe trait boundary जोड़ना