- 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 की ज़रूरत पड़ती है
asyncRust में data कोSendके रूप में move होना पड़ता है या'staticreferences के रूप में संभालना पड़ता है, औरasyncकी infectious प्रकृति के कारण ये constraints पूरे codebase में बार-बार सामने आते हैंArccompile की समस्या हल कर दे, तब भी objects और resources की lifetimes को धुंधला बना देता है; इसके बाद recursiveasync, 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/awaitmodel इस्तेमाल करता है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 होना चाहिए, या'staticlifetime वाले references के जरिए pass होना चाहिए asynccode में कई tasks का common state share करना आम है, इसलिए बिना copying data move करने का तरीका अक्सर fit नहीं बैठता- references भी कठिन हैं, और future की lifetime को “हमेशा के लिए” से छोटा restrict करने वाला
thread::scopeजैसा counterpart नहीं है asyncinfectious है, इसलिएasyncfunction को 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 को
.awaitpoints तक आगे बढ़ने वाली state machine में बदल देता है- recursive
asyncfunction recursively defined type बन जाता है - जो user बस खुद को call करना चाहता है, उसे manually boxing करनी पड़ती है या async-recursion जैसा crate इस्तेमाल करना पड़ता है
- recursive
- future await होने से पहले कुछ नहीं करता
- task runtime के thread pool में काम शुरू करता है और completion दिखाने वाला future return करता है
- future के अंदर blocking code call करने से रोकने वाला कोई mechanism नहीं है
- यह भी नहीं रोकता कि ऐसी call जिस runtime thread पर चल रही है, उसे block कर दे
- यह
asyncइस्तेमाल करने के core purpose से टकराता है
सामान्य Rust, Haskell, Go के अंतर
asyncRust का स्वाद “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 टिप्पणियां
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 को
Arcreference counting से manage किया जाता है, और Rend3/WGPU/Vulkan के जरिए meshes और textures को GPU पर upload किया जाता हैअगर यह C++ में किया होता तो लगातार crashes से लड़ना पड़ता, लेकिन Rust में memory-related crashes साल में लगभग एक बार होते हैं, और वे भी आम तौर पर किसी और के
unsafecode की वजह से। अपने code में मैंनेunsafeको ban कर रखा है, और compile करना मुश्किल है, लेकिन एक बार हो जाए तो यह “बस चल जाता है”, इसलिए मुझे लगता है कि यह concurrency debugging से कहीं बेहतर हैशिकायतें भी हैं। Rust data races के खिलाफ मजबूत है, लेकिन deadlocks नहीं रोकता, इसलिए call paths के साथ lock order track करने वाला static analyzer चाहिए।
asynccompute-heavy work और अलग-अलग priority वाले threads के लिए फिट नहीं है, फिर भी dependencies के जरिए बार-बार अंदर घुस आता है। single ownership के साथ back-references वाला आम patternRcऔर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 साल और काम चाहिए
asyncमें समस्याएँ होने के बावजूद कुल मिलाकर फायदे कहीं ज्यादा हैंLinux के
lockdepकी तरह यह analyze किया जा सकता है कि एक lock पकड़े होने पर कौन-सा दूसरा lock पकड़ा जाता है, और असल में रुकने से पहले ही risky combinations बता सकता है। complex locks में “यह lock class हमेशा address order में ली जाती है” जैसी annotations चाहिए होंगी, लेकिन implementation संभव लगती हैdata access के बाहर अभी भी race conditions हो सकती हैं: https://news.ycombinator.com/item?id=23599598
कई 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 दे सकता है
asyncRust को लेकर स्थिति थोड़ी अजीब हैअगर आप
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 करें, इस पर बहुत सारे लेख हैं। अगर आपasyncruntime 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 ठीक से लिखना पड़ता हैलेकिन इस तरह की coding
asyncJavaScript से काफी अलग है, जो synchronous code पर green threads लगाने जैसी लगती है। लोग familiar तरीके से code लिखना चाहते हैं, इसलिए शायद Rust मेंArcऔरRwLockवाले रास्ते पर चले जाते हैंसमस्या को tasks के बीच बहने वाले data के रूप में structure करना, उन्हें queues से जोड़ना और shared state से बचना — चाहे कोई भी language हो, multi-threading संभालने का बेहतर तरीका है
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 कहीं ज़्यादा सीमित हैंstackless coroutines चुनने से
async/awaitऔर colored functions की समस्या पैदा हुई, और लेख में बताई गई friction आई। Go stackful coroutines इस्तेमाल करता है, इसलिए उसमें यह समस्या नहीं हैRust ने भी शुरुआत में stackful coroutines पर विचार किया था, लेकिन coroutine preemption runtime की ज़रूरत और लागत ज़्यादा मानकर stackless model अपनाया। लेकिन ज़्यादातर लोग runtime-less
asyncRust नहीं, बल्कि Tokio इस्तेमाल करते हैं, और Tokio असल में वह लगभग सब कुछ करता है जो वह runtime करता जिससे बचना थाइसलिए अधिकांश
asyncRust users को दोनों तरफ़ की खराबियाँ मिलती हैं। Embedded side में कुछ लोग बहुत पतले runtime के साथasyncRust इस्तेमाल करते हैं, लेकिन उनकी संख्या भी कम है और वे भी पूरी तरह आश्वस्त नहीं हैंreqwestलाता है, वहh2लाता है, और फिर वहtokioलाता हैasyncइस्तेमाल करने की वजह बचती भी है या नहींJava user के तौर पर, मैं पूरे asynchronous paradigm को छोड़कर code को virtual threads पर आधारित blocking model में फिर से लिखने की कोशिश कर रहा हूँ, जहाँ blocking ठीक है
asyncइतने सारे crates में फैल चुका है कि पूरा programasyncबन जाता है, या कम से कम बहुत से कामों के लिए Tokio पर निर्भर हो जाता हैWeb server चाहिए तो मानो
async + tokioलो वरना निकलो, और SQL connector भी अगर async नहीं चाहिए तो खुद लिखो—ऐसा माहौल है। हर कोईasyncसे आने वाली समस्याओं को अलग-अलग तरीके से solve कर रहा है, औरasyncclosures जैसी चीज़ें compiler के लिए नर्क का दरवाज़ा खोलती महसूस होती हैंRust खुद और compiler problems solve करने में मदद करते हैं, यह अच्छी बात है, लेकिन ecosystem का “
asyncनहीं तो खुद बनाओ” के करीब होना पर्याप्त नहीं हैfuturescrate में बेहतर asynchronous primitives होते तो दर्द बहुत कम हो सकता थाexecutor को implement करने वाली traits, या synchronous code से asynchronous code चलाने के लिए basic blocking executor जैसी चीज़ों की ज़रूरत है। अभी multiple async runtimes support करने वाली library बनाना ही इतना कठिन है कि अंत में लोग केवल Tokio support करते हैं, या बहुत हुआ तो
async-stdजोड़ देते हैंमैं
asyncRust 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 की आलोचना करने जैसा है जो अंततः पूरी होगी
यह भी चिंता है कि 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} | state3Arcकी lifetime अज्ञात नहीं होती, बल्कि यह इस बात से तय होती है कि उसे कहाँ और कैसे रखा गया हैइस लेख में जो disconnect है, वह शायद इस वजह से है कि लेखक Rust सीखकर भाषा के हिसाब से काम करने के बजाय, garbage collection जैसे पुराने सोच के model को Rust पर जबरन लागू करना चाहता है। नई भाषा सीखते समय यह आम trap है, लेकिन Rust में लोग खास तौर पर ज़्यादा बार इसमें अटकते हैं
लेकिन यह compile time पर object lifetime को statically सीमित करने की borrow checker की मंशा से लगभग उल्टा है
असल में मेरा अनुभव इसके लगभग उलट रहा। C, C++, Rust में करीब 10 साल system programming करने के बाद, मौजूदा नौकरी में Haskell का काफी इस्तेमाल करने लगा, और यह समझना काफी eye-opening था कि बड़े language runtime और garbage collection कुछ problem domains में कोई राक्षस नहीं हैं
asynctransformation, 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 किया जा सकता हैलेखक को शायद पता है कि
Arcक्या है और कैसे काम करता है; मुख्य बात यह लगती है कि Rustasyncमें 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 होती हैArcmove या borrow किया जा सकता है, और reference count को छुए बिना भी इस्तेमाल किया जा सकता हैकई मामलों में यह implicit reference-counting languages के objects से कहीं सस्ता होता है
मुझे Rust पसंद है, लेकिन
asyncएक mess है, और async code को sync code की तरह नहीं लिखा जा सकतामुझे यह यकीन बढ़ता जा रहा है कि दोनों को मिलाना बुरा विचार है, और शायद Go-style approach सही हो सकती है जिसमें सब कुछ synchronous रखा जाता है और केवल एक
asyncchannel primitive दिया जाता हैअभी मैं
Futureimplement करने वाली struct में synchronous methods call कराने के लिए logic wire कर रहा हूँ, और यह काफी दिलचस्प चुनौती है। zero-cost async abstraction को users के लिए कुछ हद तक आसान बनाया जा सकता है, लेकिन दर्द library developers को उठाना पड़ता हैasyncend users के लिए भी निश्चित रूप से दर्दनाक है; ऐसा लगता है जैसे Rust की core features—lifetimes और explicit types—के बिना कोई अलग भाषा इस्तेमाल कर रहे हों, जिस पर ढेर साराPinछिड़क दिया गया होscoped fibers चलाए नहीं जा सकते, इसलिए आखिरकार बहुत सारे
Arcलगाने पड़ते हैं;Pinकोunsafeके बिना इस्तेमाल करना मुश्किल है; और async function में बहुत छोटा बदलाव पूरे codebase के futures को!Sendबना सकता हैasyncकी ज़्यादातर मुश्किलें हल हो सकती हैं। देखना होगा यह कैसे विकसित होता हैAsync Everything एक खराब भाषा है
async/awaitJavaScript में सही 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=oNnITaBseYQJavaScript को लेकर वह खुद काफी 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 के रूप में यह उतना ही खराब होता जाता है, इसलिए यहां की गई
asyncRust की आलोचना अधिकतर सही हैलेख
asyncRust की जटिलता और कठिनाई को अच्छी तरह समझाता है, लेकिन यह भी महत्वपूर्ण है कि 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छिड़कने के बजाय मैं यहunsafecrate इस्तेमाल करता हूं: https://docs.rs/async-scoped/latest/async_scoped/इतना करने से C++ में बनने वाले bugs के 99% पकड़े जाते हैं, इसलिए यह reasonable trade-off है। safe तरीके से non-
'staticfuture implement करने का काम भी चल रहा है, उम्मीद है सफल होगाएक और बड़ी समस्या यह है कि
async traitफिलहाल boxed future मांगता है, जिससे हर function call boundary परmalloc/freeजुड़ जाता है, लेकिन यह इस साल के fix roadmap में है“बस channel इस्तेमाल करो” वाली सलाह भी बड़े codebase में control flow को हर तरफ बिखेर देती है। channels modern
GOTOजैसे लगते हैं, और मैं भी उनका इस्तेमाल करता हूं, लेकिन जब केवल कुछ काम parallel में चलाकर उनके complete होने का इंतजार करना हो, तब उनका ज्यादा उपयोग नहीं करता'staticनहीं होता, बल्कि runtime की concurrency का लाभ उठाते हुएspawnकिए जा सकने वाले future सिर्फ'staticहोते हैंfuture को
poll()किए जाने के लिएPinहोना चाहिए, औरPinकिया गयाT: !UnpinआखिरकारDropcall करना चाहिए: https://doc.rust-lang.org/std/pin/#drop-guaranteecompiler के
asyncfeature से बने 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 तरीके से अलग करना संभव नहीं, या इसकी ज्यादा परवाह करने की जरूरत नहीं होतीRcहटाना, या फिर infectiousunsafetrait boundary जोड़ना