Async Rust कभी MVP स्टेटस से बाहर नहीं निकला
(tweedegolf.nl)- Async Rust executor-स्वतंत्र कोड को server और microcontroller पर साथ चलाने देता है, लेकिन compiler द्वारा बनाए गए state machine की वजह से खासकर embedded में binary size बढ़ना साफ़ दिखता है
bar()जैसे साधारण उदाहरण में भी, जहाँ 2 await points हैं, 360 lines of MIR औरUnresumed,Returned,Panicked,Suspend0,Suspend1states बनते हैं, जबकि synchronous version को सिर्फ 23 lines चाहिए- completed future को दोबारा poll करने पर
panicकी जगहPoll::Pendingलौटाने से unsafe behavior के बिना contract पूरा किया जा सकता है, और प्रयोग में embedded firmware की binary size 2%~5% कम हुई - await के बिना
async { 5 }भी अभी default 3-state state machine बनाता है, लेकिन हर बारPoll::Ready(5)लौटाने के लिए optimize करने पर embedded binary size 0.2% कम हुई - प्रस्तावित Project Goal का उद्देश्य compiler में release mode में completion के बाद panic हटाना, await-रहित async block का state machine हटाना, single-await future inlining, और identical state folding को आगे बढ़ाना है
Async Rust की compiler-स्तरीय bloat समस्या
- Async Rust executor-स्वतंत्र कोड को server और microcontroller पर एक साथ चलाने देता है, लेकिन छोटे microcontroller पर binary size बढ़ना खास तौर पर नज़र आता है
- Rust ब्लॉग ने async/await को zero-cost abstraction के रूप में पेश किया था, लेकिन async वास्तव में काफी bloat पैदा करता है; यही समस्या desktop और server पर भी होती है, बस वहाँ memory और compute resources ज़्यादा होने से कम दिखती है
- async code लिखते समय bloat से बचने के workaround के बाद, अब इस समस्या को compiler में हल करने के लिए Project Goal जमा किया गया है
- future का ज़रूरत से बड़ा हो जाना और copy ज़्यादा होना इस दायरे में शामिल नहीं है
- यह समस्या पहले से ज्ञात है, और इसे आंशिक रूप से संबोधित करने वाला PR खुला है: https://github.com/rust-lang/rust/pull/135527
generated future की संरचना
- उदाहरण code में
foo()async { 5 }लौटाता है, औरbar()foo().await + foo().awaitकरता है- Godbolt उदाहरण: godbolt
barमें 2 await points हैं, इसलिए state machine में कम-से-कम 2 states चाहिए, लेकिन वास्तव में इससे ज़्यादा states बनते हैं- Rust compiler कई passes में MIR dump कर सकता है, और
coroutine_resumepass आख़िरी async-विशेष MIR pass है- async MIR में मौजूद रहता है, लेकिन LLVM IR में नहीं, इसलिए async का state machine में रूपांतरण MIR pass में होता है
barfunction 360 lines of MIR बनाता है, जबकि synchronous version सिर्फ 23 lines इस्तेमाल करता है- compiler द्वारा output किया गया
CoroutineLayoutअसल में enum-जैसे state set के बराबर हैUnresumed: शुरुआती stateReturned: completed statePanicked: panic के बाद की stateSuspend0: पहला await point, जहाँfoofuture स्टोर होता हैSuspend1: दूसरा await point, जहाँ पहला result और दूसराfoofuture स्टोर होता है
Future::pollएक safe function है, इसलिए future पूरा होने के बाद भी अगर उसे फिर बुलाया जाए तो वह UB नहीं पैदा करना चाहिए- अभी
Suspend1के बाद यहReadyलौटाता है और future कोReturnedstate में बदल देता है - इस state में दोबारा poll करने पर panic होता है
- अभी
Panickedstate शायद इसीलिए है कि async function panic होने के बाद, अगर उसेcatch_unwindसे पकड़ा जाए, तो उस future को फिर poll न किया जा सके- panic के बाद future अधूरी state में हो सकता है, इसलिए दोबारा poll करना UB तक ले जा सकता है
- यह mechanism mutex poisoning से काफ़ी मिलता-जुलता है
Panickedstate की यह व्याख्या पूरी तरह दस्तावेज़ित नहीं मिली, इसलिए इस पर लगभग 90% भरोसा बताया गया है
completion के बाद poll पर क्या panic ज़रूरी है?
Returnedstate का future अभी panic करता है, लेकिन यह अनिवार्य नहीं है- ज़रूरी शर्त सिर्फ यह है कि UB न हो
- panic अपेक्षाकृत महँगा है, और ऐसा path जोड़ता है जिसमें side effects होते हैं जिन्हें optimize करके हटाना कठिन है
- completed future को फिर poll करने पर
Poll::Pendingलौटाने से unsafe behavior के बिनाFuturetype का contract पूरा किया जा सकता है - compiler में यह बदलाव प्रयोग के तौर पर करने पर async embedded firmware में 2%~5% binary size reduction देखी गई
- इस behavior को integer overflow के
overflow-checks = falseजैसी switch के रूप में देने का प्रस्ताव है- debug builds में गलत behavior तुरंत दिखाने के लिए panic जारी रहेगा
- release builds में छोटे future मिल सकते हैं
panic=abortउपयोग करने परPanickedstate को ही हटाया जा सकने की संभावना है, लेकिन उसके असर की और जाँच चाहिए
await न होने पर भी हमेशा state machine बनता है
foo()सिर्फasync { 5 }लौटाता है, इसलिए manual implementation का सबसे optimized रूप ऐसा future होगा जिसमें कोई state न हो और जो हमेशाPoll::Ready(5)लौटाए- लेकिन compiler-generated MIR में अभी भी
Unresumed,Returned,Panickedनाम की 3 base states मौजूद रहती हैं- poll के समय current state का discriminant देखा जाता है और उसी के हिसाब से branch होती है
- completion के बाद फिर poll करने पर
`async fn` resumed after completionassert से panic होता है
- इस स्थिति में state machine बनाए बिना हर बार
Poll::Ready(5)लौटाने के लिए optimize किया जा सकता है - compiler में इसे प्रयोगात्मक रूप से लागू करने पर embedded binary size 0.2% कम हुई
- बचत बड़ी नहीं है, लेकिन optimization सरल है इसलिए इसे अपनाना उपयोगी हो सकता है
- यह optimization behavior थोड़ा बदलती है, लेकिन असर सिर्फ उन executors पर पड़ेगा जो contract का पालन नहीं करते
- अभी compiler बाद के poll पर panic करता है
- optimization के बाद future हमेशा
Readyलौटाएगा
सिर्फ LLVM काफ़ी नहीं है
- कभी-कभी MIR output अक्षम होने पर भी LLVM बहुत कुछ साफ़ कर सकता है, लेकिन इसके लिए सीमित शर्तें हैं
- future काफ़ी simple होना चाहिए
opt-level=3इस्तेमाल करना चाहिए
- future जटिल होने पर LLVM इन्हें हटा नहीं पाता, और idiomatic async Rust code में future गहराई से nested होते हैं, इसलिए complexity तेज़ी से बढ़ती है
- embedded या wasm जैसे environments में, जहाँ size optimization आम है, LLVM सब कुछ optimize नहीं कर पाता
- Godbolt उदाहरण: https://godbolt.org/z/58ahb3nne
- generated assembly में LLVM यह जानता है कि
foo5 लौटाता है, लेकिनbarके answer को 10 तक optimize नहीं कर पाता fooके poll function call भी बचे रहते हैं- वजह वे संभावित panic paths हैं जिन्हें compiler पूरी तरह समझ नहीं पाता
- LLVM यह नहीं जानता कि
fooवास्तव में सिर्फ एक बार call होगा और panic नहीं करेगा
- generated assembly में LLVM यह जानता है कि
- IR में panic branch को comment out करने पर optimization बेहतर हो जाती है: https://godbolt.org/z/38KqjsY8E
- LLVM से बाद में optimization की उम्मीद करने के बजाय, compiler को LLVM को बेहतर input देना चाहिए
future inlining ठीक से नहीं होती
- inlining महत्वपूर्ण है क्योंकि उसके बाद और optimization passes संभव होते हैं, लेकिन generated Rust futures अभी शुरुआती चरण में inline नहीं होतीं
- हर future की implementation बनने के बाद LLVM और linker को inline करने का मौका मिलता है, लेकिन ऊपर बताई समस्याओं के कारण तब तक बहुत देर हो चुकी होती है
- सबसे सीधा inlining अवसर वह रूप है जहाँ
bar()सिर्फfoo(blah).awaitकरता है- abstraction के लिए trait उपयोग करते समय यह pattern अक्सर दिखता है
- अभी compiler
barके लिए state machine बनाता है और उसके भीतरfoostate machine को call करता है - ज़्यादा कुशल रूप में
barखुदfoofuture बन सकता है
- preamble और postamble होने पर मामला अधिक जटिल है
- उदाहरण:
bar(input)पहलेinput > 10सेblahबनाता है, फिरfoo(blah).awaitकरता है, और result पर* 2लागू करता है - async function को दूसरे signature में बदलते समय, खासकर trait implementations में, यह रूप आम है
- उदाहरण:
- इस तरह के
barको भी अपनी अलग async state की ज़रूरत नहीं होती- single await point के पार संरक्षित होने वाला data,
fooमें captured value के अलावा कुछ नहीं है - हालांकि
barसीधेfooनहीं बन सकता, लेकिन अपनी ज़्यादातर statefooपर निर्भर कर सकता है
- single await point के पार संरक्षित होने वाला data,
- manual implementation में
BarFutके पासUnresumed { input }औरInlined { foo: FooFut }states हो सकती हैं- पहले poll पर preamble चलाकर
foo(blah)बनाया जाता है और state कोInlinedमें बदला जाता है - उसके बाद
foo.poll(cx)के result पर postamble लागू किया जाता है
- पहले poll पर preamble चलाकर
- अगर पहले await point तक का code पहले से चलाया जा सके, तो
Unresumedstate भी हटाई जा सकती है, लेकिन यह guarantee है कि future poll होने से पहले कुछ नहीं करता, इसलिए इसे बदला नहीं जा सकता - अगर poll हो रहे future के गुणों के बारे में पूछताछ की जा सके, तो और inlining optimizations संभव हैं
- उदाहरण के लिए, अगर यह पता हो कि future पहले poll पर हमेशा ready लौटाता है, तो caller future को उस await point की state बनाने की ज़रूरत नहीं होगी
- इस तरह की optimization को recursive ढंग से लागू करने पर कई futures को बहुत सरल state machines में समेटा जा सकता है
- मौजूदा
rustcसंरचना में, हर async block अलग-अलग transform होता है और बाद में संबंधित data सुरक्षित नहीं रहता, इसलिए ऐसी queries संभव नहीं दिखतीं - future inlining का अभी प्रयोग नहीं हुआ है, लेकिन उम्मीद है कि यह binary size और performance दोनों में बड़ा लाभ देगी
identical state folding
- async block के हर await point पर state machine में एक अतिरिक्त state बनती है
- नीचे जैसा code स्वाभाविक है, लेकिन दोनों branches में वही async function await होने से 2 identical states बनती हैं
CommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- इस स्थिति में
CoroutineLayoutमेंsend_responseके उसी coroutine type को रखने वाले_s0,_s1अलग-अलग बनते हैं, औरSuspend0,Suspend1दो states तैयार होती हैं - इस function की MIR 456 lines है, और कई basic blocks वस्तुतः duplicate हैं
- अगर code को पहले सिर्फ response value calculate करने और फिर एक बार
send_response(response).awaitकरने के लिए manually refactor किया जाए, तो duplicate states हट जाती हैंCommandId::Aके लिए123CommandId::Bके लिए456- उसके बाद
send_response(response).await
- refactor के बाद
CoroutineLayoutमें सिर्फ एक stored future बचता है और सिर्फ एकSuspend0state रहती है - कुल MIR length घटकर 302 lines रह जाती है और duplication हट जाती है
- इसलिए ऐसे optimization pass उपयोगी लगते हैं जो identical code paths और states को पहचानकर एक में fold कर सकें
- यह optimization future inlining pass के साथ अच्छी तरह जुड़ सकती है
प्रयोग लिंक और अतिरिक्त benchmark
- दोनों प्रयोग साथ लागू करने पर
smolexecutor के साथ x86 synthetic benchmark में लगभग 3% performance improvement मिला - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Project Goal के लिए समर्थन अनुरोध
- compiler में यह काम आगे बढ़ाने के लिए इसे Project Goal के रूप में जमा किया गया है: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- funding के बिना बहुत काम आगे बढ़ाना मुश्किल है, इसलिए उन companies या organizations के आंशिक या पूर्ण समर्थन की ज़रूरत है जिन्हें इस काम से लाभ होगा
- संपर्क:
dion@tweedegolf.com - काम का दायरा और ज़रूरी funding लचीली है, लेकिन अनुमान है कि €30k में पूरा या बड़ा हिस्सा पूरा किया जा सकता है
2 टिप्पणियां
Hacker News टिप्पणियाँ
मैं मानता हूँ कि शीर्षक थोड़ा बढ़ा-चढ़ाकर लिखा गया है, लेकिन लेख अच्छी तरह लिखा गया है और बात भी साफ़ पहुँचती है
अभी Rust async पर बहुत मज़बूत राय देने लायक मेरा अनुभव नहीं है, लेकिन कुछ बातें ध्यान खींचती हैं
अच्छी बात यह है कि explicit runtime रखा जा सकता है। पूरे प्रोजेक्ट को async से दूषित करने के बजाय, डिफ़ॉल्ट को synchronous रखा जा सकता है और सिर्फ I/O “boundary” पर runtime इस्तेमाल किया जा सकता है
जिस प्रोजेक्ट पर मैं काम कर रहा हूँ, उसमें यह तरीका अच्छी तरह फिट बैठा, और Zig जो I/O code में रणनीति अपनाता है, उससे भी काफ़ी मिलता-जुलता लगता है। इस मामले में function color problem भी ज़्यादातर हल हो गई, और I/O तथा CPU-केंद्रित code को सख़्ती से अलग रखना था, इसलिए explicit I/O runtime स्वाभाविक लगा
बुरी बात यह है कि पूरा ecosystem tokio पर कुछ ज़्यादा ही निर्भर दिखता है। यह वैसा है जैसे Java में GC वैकल्पिक हो, लेकिन व्यवहार में सब एक ही third-party GC runtime इस्तेमाल करें, और कोई भी library लाओ तो वही runtime मजबूरी बन जाए। ऐसी केंद्रीय निर्भरता स्वस्थ नहीं है
workstation processor पर async runtime की ज़रूरतें और RP2040 जैसे वातावरण की ज़रूरतें बहुत अलग हैं। फिर भी backend बदला जा सकता है, इसलिए जब छोटे ARM M0 microcontroller के लिए async I/O code लिखा जाता है, तब embedded-केंद्रित runtime embassy इस्तेमाल करने पर code दूसरे environments वाले code जैसा ही दिखता है
क्योंकि वही trait और interface इस्तेमाल होते हैं, runtime की details की कम चिंता करनी पड़ती है। छोटे RTOS के इस्तेमाल या खुद async environment बनाने की तुलना में यह काफ़ी अच्छा है
embassy में async code लिखते हुए जो सीखा, उसे दूसरे क्षेत्रों में भी ले जाया जा सकता है
tokio standard library का हिस्सा नहीं है, फिर भी अच्छी तरह maintain हो रहा है, इसलिए अभी की स्थिति ठीक लगती है। उल्टा, अगर यह standard library में चला जाए तो दूसरे executors इस्तेमाल करना कठिन हो सकता है, और standard library को दूसरे platforms पर port करना भी मुश्किल हो सकता है
बेशक, हो सकता है यह चिंता निराधार हो
logging अब काफ़ी हद तक slf4j पर आ गई है, लेकिन अब भी कुछ libraries दूसरी चीज़ें इस्तेमाल करती हैं; common utilities पहले Apache Commons हुआ करता था, अब Guava ज़्यादा दिखता है
JSON कुछ हद तक Jackson पर आकर स्थिर हुआ, लेकिन Gson या Simple-json भी आम हैं; और nullability annotations भी कभी औपचारिक नहीं हुए JSR-305 की अनौपचारिक distribution से checker framework होते हुए हाल में JSpecify की ओर बढ़ रहे हैं
ऐसी बुनियादी चीज़ें language को देनी चाहिए, ताकि fragmentation और de facto standard libraries की भरमार से बचा जा सके
libraries को executor-independent तरीके से लिखना बहुत कठिन नहीं है, लेकिन लगातार सावधानी चाहिए, और community के ज़्यादातर हिस्सों में यह हमेशा पालन नहीं होता
शानदार लेख है। मुझे ऐसे optimization deep dive बहुत पसंद हैं, और उम्मीद है कि project goals भी अच्छी तरह पूरे हों
मुझे अक्सर लगा है कि compiler “तुच्छ” मामलों के optimization पर बहुत बड़ा प्रयास नहीं लगाता
लेकिन शीर्षक, सामग्री की तुलना में, बहुत नाटकीय है। “Async Rust Optimizations the Compiler Still Misses” भी होता तो मैं क्लिक करता
अब traits और closures में async इस्तेमाल किया जा सकता है, लेकिन वह type system का update है, async मशीनरी का बदलाव नहीं। Waker भी थोड़ा आसान हुआ है, लेकिन वह std/core पक्ष के सुधार जैसा है
मेरी समझ के अनुसार, async Rust को landing कराने वाले लोगों में काफ़ी burnout हुआ और उनकी सक्रियता कम हो गई, और उसके बाद ज़िम्मेदारी उठाने वाले बहुत कम लोग आए। हालांकि Google के लोगों ने captured variables की memory layout optimize करने वाला एक PR खोला है, यह काफ़ी उत्साहजनक है
मैं और मेरे सहकर्मी async बहुत इस्तेमाल करते हैं, इसलिए शायद हमें ही यह काम करना चाहिए, या कम से कम शुरुआत करनी चाहिए। “मुफ़्त” शायद उस अर्थ में मुफ़्त है जैसे एक पिल्ला मुफ़्त होता है
इसलिए हाँ, शीर्षक थोड़ा clickbait है, लेकिन मैं उसे वापस लेने का इरादा नहीं रखता
लेखक तुच्छ functions के overhead पर कुछ ज़्यादा ही अटका हुआ लगता है। उसे “panic” और “returned” states के overhead से समस्या है, लेकिन वह कोई बड़ी बात नहीं है
ज़्यादातर उपयोगी async blocks इतने बड़े होते हैं कि error-case overhead दब जाता है
inlining की कमी पर शायद कुछ बात बनती है। लेकिन बहुत बड़ी संख्या में activities को सीमित करने वाली चीज़ आम तौर पर हर activity की state space होती है
कुल मिलाकर async मुझे एक अधपका विचार लगता है। सामान्य code भी पहले से asynchronous था
अगर async काम का इंतज़ार करना हो, तो thread तैयार होने तक सो जाता है, और kernel इसे abstract कर देता है। लेकिन लोगों को logical threads के रूप में code संगठित करना पसंद नहीं आया, इसलिए event callbacks system जोड़ दिया गया, और फिर यह समझ आया कि callbacks पर reasoning करना कठिन है और sequential control बेहतर है
इसलिए मुझे लगता है कि threads ही सही programming model थे
अब language runtimes portability और performance के कारण “green threads” पसंद करते हैं, लेकिन ज़्यादातर languages इन्हें ठीक से उपलब्ध नहीं करातीं। उसकी जगह async/non-async color problem, scheduling, priority, non-preemption जैसी समस्याएँ आती हैं। यह 1970 के दशक से भी बदतर scheduling और process model है
async code भी अक्सर इस तरह लिखा जाता है कि व्यक्त की जा सकने वाली concurrency अधिकतम नहीं हो पाती। उदाहरण के लिए, “N I/O operations सब एक साथ चलाओ” की जगह “हर task X के लिए await process(x)” जैसा लिखना
लेकिन threads की दुनिया में concurrency की यह समस्या और गंभीर हो जाती है। threads मूलतः इतने भारी होते हैं कि concurrency को कुशलता से व्यक्त करना कठिन हो जाता है, और उस दिशा में optimize करने का कोई अच्छा तरीका भी नहीं है
यह कोई नई सीख नहीं है। work-stealing executors के बारे में बहुत पहले से पता है कि वे पारंपरिक threads की तुलना में latency काफ़ी कम रखते हैं और P99 भी ज़्यादा स्थिर होता है। 2000 के शुरुआती दशक में Apple ने GCD इसी वजह से बनाया था
threads kernel scheduler को workload समझने के लिए ज़रूरी समृद्ध जानकारी नहीं दे पाते, और kernel threads सूक्ष्म स्तर की concurrency पाने के लिए बहुत भारी mechanism हैं। pure computation नहीं बल्कि I/O या mixed workload हो तो स्थिति और ख़राब होती है
हर program को इस स्तर की performance की ज़रूरत नहीं होती, लेकिन उतने ही प्रयास में ऊँचा performance baseline हासिल करना बहुत आसान हो जाता है, और वास्तव में ऐसी latency व throughput मिल सकती है जिन्हें पारंपरिक approach पकड़ नहीं पाती
async सही दिशा में है, इसका संकेत io_uring से भी मिलता है। kernel का high-performance I/O approach, io_uring, पारंपरिक threading और system calls से बिल्कुल अलग है, और completion handling भी async concurrency के कहीं ज़्यादा करीब है। हाँ, async/await अकेले async tasks के बीच के संबंध व्यक्त करने के लिए पर्याप्त “colors” नहीं देता, इसलिए इसका पूरा लाभ उठाना कुछ कठिन हो जाता है
पिछली बार जब मैंने coroutine/scheduling code के साथ काम किया था, तब तुरंत समाप्त होने वाला thread बनाकर join करने में लगभग 200µs लगे थे, जबकि अपनी green thread बनाकर schedule करके उसका इंतज़ार करने में लगभग 400ns लगे
किसी के फिर से absurd रूप से जटिल async framework डिज़ाइन करने के लिए 10 साल इंतज़ार करने की ज़रूरत नहीं है। किसी भी systems language में assembly की 20 lines से आप खुद green threads/stackful coroutines बना सकते हैं
bandwidth-heavy code का optimization असल में scheduling design की समस्या है। पारंपरिक multithreading model में scheduling पर सीमित नियंत्रण होता है, जबकि async model में इसे लगभग पूरी तरह नियंत्रित किया जा सकता है
अच्छी तरह optimized async schedule, उसी bandwidth-heavy workload में, बराबर की multithreaded architecture से बहुत तेज़ हो सकती है; तुलना ही नहीं होती
आज का ज़्यादातर high-performance code bandwidth-heavy है, और async ऐसे workloads को optimize करना आसान बनाने के लिए मौजूद है
जब concurrent processing को test करना हो और देखना हो कि race conditions सही तरह handle हो रही हैं या नहीं, तब callbacks बहुत आसान हैं क्योंकि वे scheduling को नियंत्रित करने देते हैं। हर callback एक अलग unit दिखाता है, इसलिए यह समझना आसान हो जाता है कि कौन-सी events reorder की जा सकती हैं, और अलग-अलग क्रमों की जाँच भी आसान होती है
इसके विपरीत, threads में क्रम को नज़रअंदाज़ कर देना आसान होता है, और यह सोचना भी छूट जाता है कि दूसरे thread की जटिलता मौजूदा thread को कब प्रभावित कर सकती है। यह सरल नहीं, बल्कि सरलीकृत मॉडल जैसा है
और concurrency scenarios को सच में बदलकर test करना भी कठिन है, जब तक आप artificial barriers डालकर threads रोकें नहीं, या I/O को stubs में बदलकर order नियंत्रित करने वाले callbacks वाला mock पास न करें
callbacks की समस्या यह है कि captured call stack, logical call stack नहीं होता। meaningful call stack पाने के लिए, जब तक कोई library/runtime उस पर विशेष मेहनत न करे, अच्छे error definitions चाहिए होते हैं
बेशक, दोनों paradigms को मिलाकर दोनों के नुकसान भी साथ पाए जा सकते हैं
अगर Rust का मुख्य लक्ष्य safety है, तो मैं समझ नहीं पाता कि panic क्यों है। यह साबित कर पाना चाहिए कि code में बिल्कुल भी panic होने वाला path नहीं है
मैंने इस हफ़्ते भर यह देखा, और ऐसा program बनाना बहुत कठिन है जो यह गारंटी दे कि वह कभी panic नहीं करेगा। मेरी समझ के अनुसार panic handler लगभग 300KB का होता है, और इसे बाहर रखने का इकलौता तरीका यह है कि compile समय पर code में panic होने वाला कोई path ही न हो। compile के बाद binary में panic handler शामिल है या नहीं, यह जाँचने का तरीका हैक जैसा लगता है
unwrap और दूसरे panic operations को lint से रोका जा सकता है, लेकिन अगर no-panic Rust subset होता, तो इस लेख में उठी काफ़ी समस्याएँ गायब हो जातीं
ऐसी language के साथ काम करना निराशाजनक है जिसमें सैद्धांतिक रूप से panic कर सकने वाले operations बहुत ज़्यादा हैं, जबकि व्यवहार में वे शायद केवल bit flips जैसी स्थितियों में ही होंगे। खाली न होने वाले array को सिद्ध करने में भी यही दिक्कत है, और async में भी
अंत में या तो आपको ऐसी स्थितियों के लिए ढेर सारा error handling जोड़ना पड़ता है जो कभी होंगी ही नहीं, या फिर non-empty list pattern जैसी अजीब संरचनाएँ इस्तेमाल करनी पड़ती हैं, जहाँ पहला field और बाकी list अलग रखी जाती है। और वह संरचना भी अपना अलग bloat जोड़ती है
proof-driven usage बढ़ाने का काम भी धीरे-धीरे चल रहा है, जिसमें array के खाली न होने जैसी बातों को साबित करना शामिल है
अगर panic न हो और हर स्थिति में execution जारी रखना पड़े, तो ऐसी memory corruption जैसी स्थितियों में recovery के लिए बहुत-सी error handling डालनी पड़ेगी जहाँ invariants टूट चुके हों, और हर जगह invariants जाँचने पड़ेंगे
यह ठीक उसी तरह की समस्या है जिसकी आप चिंता कर रहे हैं: ऐसी स्थितियों के लिए विशाल error handling जो लगभग कभी घटित नहीं होंगी
इस रवैये से थकान होती है कि tools सब कुछ असफल-असंभव बना दें और खुद कोई ज़िम्मेदारी न लेनी पड़े। लोग आसान APIs चाहते हैं, और अगर वह पर्याप्त आसान न हो तो YAML में Kubernetes containers “program” करना चाहते हैं, और वह भी आसान न हो तो GCP या Amazon की click-based hosting services चाहते हैं
अंततः यह programming नहीं बल्कि fail-proof apps का उपभोग करना चाहने जैसा रवैया बन जाता है, और ऐसी जीवनशैली आखिरकार उन लोगों के सहजीवी श्रम पर टिकी होती है जो वास्तव में चीज़ें बनाते हैं
ऐसी बदसूरत लेकिन ज़रूरी चर्चाएँ C++ में भी काफ़ी समय से चलती रही हैं
Rust में async आने के समय से ही उसका यह infectious character मुझे पसंद नहीं था
मैं चाहता हूँ कि Rust सफल हो, और अगर ऐसे लोग बढ़ें तो Rust का भविष्य और उज्ज्वल हो सकता है
मैंने हाल ही में Rust async का काम शुरू किया है, और अभी मेरी मुख्य समस्या code duplication है
हर उस function को दोहराकर लिखना पड़ता है जहाँ asynchronous API और blocking API दोनों को support करना हो।
maybe-asyncजैसा कुछ होना अच्छा लगताइससे बचने के लिए मैंने maybe-async, bisync जैसे crates देखे, लेकिन सबमें कोई न कोई समस्या या कड़ी सीमाएँ थीं
asyncयाconstजैसे keywords के संदर्भ में functions को generic बनाया जा सकेअभी ऐसे code के लिए जो sync और async दोनों में जीना चाहता हो, सबसे अच्छा विकल्प sans-io है। Fireguard के Thomas Eizinger ने इस pattern पर अच्छी पोस्ट लिखी है[1]
यह pattern सिर्फ sync/async समस्या को साफ़ ढंग से हल नहीं करता, बल्कि testing भी आसान बनाता है, और DST जैसी तकनीकों की ओर रास्ता भी खोलता है[2]
मैंने भी इस विषय पर एक लेख लिखा है[3], जिसमें ज़ोर दिया है कि समस्या सिर्फ async बनाम sync नहीं, बल्कि अलग-अलग executors तक फैली हुई एक बड़ी समस्या है
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
asyncfunction पहले से हीmaybe-asyncहैfn -> voidऔरfn -> Futureमें फ़र्क बस इतना है कि पहला तुरंत अंत तक चल जाता है, जबकि दूसरा बाद में जाकर पूरा हो सकता हैअगर आप async function को blocking तरीके से चलाना चाहते हैं, तो blocking executor इस्तेमाल कर लें
मुझे यह लेख इसलिए पसंद है क्योंकि इसने 2026 Rust goals तक देखने का मौका दिया
हमारी team Rust इस्तेमाल करती है, लेकिन ज़रूरी काम करने के लिए हमें बहुत गहराई में जाने की ज़रूरत नहीं पड़ी। फिर भी ऐसी language को ground-up विकसित होते देखना मज़ेदार है जिसमें community feedback बहुत हो
C++ में मुझे ऐसा प्रवाह कम महसूस हुआ, और दूसरे क्षेत्रों में चीज़ें कैसे चलती हैं, यह भी मैं उतना नहीं जानता
एक कमी बस यह लगी कि हर goal को कुछ विशेष funding की ज़रूरत पड़ती दिखती है, इसलिए थोड़ा Kickstarter जैसा महसूस होता है। सोचता हूँ, क्या अभी तक यही सबसे अच्छा model है
project goals एक ऐसा सिस्टम है जिसमें कोई व्यक्ति या छोटा समूह यह जताता है कि वह कोई काम करना चाहता है, और Rust project volunteers से code review, सवालों के जवाब जैसी निरंतर सहायता का समय माँगता है
इसका मतलब यह नहीं कि Rust project ने खुद वह goal तय किया है, या उसका अनिवार्य रूप से समर्थन किया है
इसलिए इसे Rust का आधिकारिक roadmap मानना सही नहीं होगा; ज़्यादा सही यह कहना है कि “इस क्षेत्र में काम करना चाहने वाले contributors मौजूद हैं”
जब technology व्यावसायिक रूप से स्थापित हो जाती है, तो दुर्भाग्य से चीज़ें अक्सर ऐसे ही बहती हैं। बड़े sponsors को इस बात के लिए दोष देना कठिन है कि वे सिर्फ अपने रुचि-क्षेत्रों को fund करते हैं
ख़ुशी की बात है कि TweedeGolf की काफ़ी funding, मेरी जानकारी में, Netherlands government से आती है
नई features “बेची” जा सकती हैं। उन्हें बनाने में पैसा लगता है, लेकिन वे वास्तविक समस्याएँ हल करती हैं, और अगर उन समस्याओं की लागत feature development की लागत से अधिक हो, तो कंपनियाँ आम तौर पर भुगतान करने को तैयार होती हैं
maintenance कठिन है, लेकिन अब maintainers funds भी हैं। RustNL का fund इसका उदाहरण है: https://rustnl.org/maintainers/
ऐसे funds व्यापक और सतत काम को लक्ष्य बनाते हैं, और कई संगठन थोड़ा-थोड़ा योगदान देकर उन्हें सहारा देते हैं
यह सबसे अच्छा model है या नहीं, पता नहीं, लेकिन कम से कम कुछ हद तक काम करता दिखता है
Rust Async और Tokio के docs पढ़ें तो यह ठीक से समझाया गया है कि CPU-intensive हिस्सों को async stack में क्यों नहीं डालना चाहिए,
std::sync::Mutexजैसे बुनियादी tools को async block में कुशलता से कैसे इस्तेमाल करना है, और sync code तथा async code को कैसे जोड़ा जाएबहुत-सा code efficiency की परवाह नहीं करता या उसे उसकी ज़रूरत नहीं होती, इसलिए वह इन guidelines का पालन नहीं करता। लेकिन performance और efficiency को महत्व देने वाले बहुत से projects हैं, और production में code चलने पर ये pitfalls समझ में आते हैं। ScyllaDB इसका एक उदाहरण है
LLM भी मदद नहीं करता। वह हर चीज़ को
mainतक async बना देता है, गलत default tools इस्तेमाल करता है, और system को ठीक से design नहीं करताduplicate states को fold करना, यानी
process_commandउदाहरण की तरह match को await branch के बाहर खींच लाने वाला pattern, आज किसी भी मौजूदा async codebase में लागू किया जा सकने वाला सबसे आसान सुधार हैइसके लिए compiler work की ज़रूरत नहीं, सिर्फ refactoring चाहिए
“Future आसानी से inline नहीं होते” वाली बात पर, मैंने अपनी बनाई programming language में async function के भीतर async function calls को inline करने के लिए एक custom pass लिखा था
वह आम तौर पर अच्छी तरह काम करता है और कुछ boilerplate हटा सकता है, लेकिन परिणामस्वरूप binary size काफ़ी बढ़ जाती है
तकनीकी रूप से Rust भी वही काम कर सकता है
Lobste.rs की राय
सिर्फ़ शीर्षक देखकर जितना सोचा था, उससे कहीं ज़्यादा रचनात्मक लेख था
उम्मीद है कि जो लोग इस पर काम करना चाहते हैं, उन्हें ज़रूरी support मिले
अच्छा लगा कि इस समस्या पर काम हो रहा है। मैंने कई बार ऐसे लेख देखे हैं जिनमें कहा गया है कि अभी rustc LLVM को बहुत ज़्यादा code सौंप देता है और उम्मीद करता है कि optimizer सब संभाल लेगा, और खासकर यह लेख इस काम के लिए funding भी माँग रहा है
हे भगवान, मैं कितना मूर्ख था
मैं हमेशा सोचता था कि async किसी न किसी रूप में runtime, task tracking, और completion की जाँच के लिए polling माँगता है, इसलिए वह मूल रूप से “भारी” ही होगा। क्योंकि वह overhead शून्य तो नहीं है
मैं समझता था कि यहाँ “zero-cost abstraction” language feature की बात है, जो जोड़े गए runtime से अलग है
LLVM को देने से पहले rustc वास्तव में क्या emit करता है, यह देखने का ख़याल ही कभी नहीं आया
जो लोग async Rust से परिचित नहीं हैं, उनके लिए:
यह बात सच में सही है। async calls की nested tree भी अगर पूरी optimization से गुज़रे तो आख़िर में अंदर state machine रखने वाले एक single struct में सिमट जाती है। यह वाक़ई बहुत चतुर तरीका है
अगर release build में यह case आ जाए, तो क्या इससे किसी तरह का deadlock बनता है? या फिर हमेशा
Pendingरहने वाले कामों का इंतज़ार कर रहे tasks की वजह से leak भी हो सकता है?.awaitके साथ गलत polling नहीं की जा सकतीकुछ विचार आए:
panic=unwindपसंद नहीं है। कुछ test harnesses को छोड़कर मैंने बहुत कम ऐसे फ़ायदे देखे हैं जोpanic=abortकी तुलना में इसकी लागत को सही ठहरा सकें। यहाँ तक कि test harness में भी Linux पर कुछ उलझे हुए तरीके सेcloneका इस्तेमाल करकेpthread_joinकी जगह executing thread कोwaitकरने जैसी मिलती-जुलती पसंद लागू की जा सकती है। हो सकता है इस हिस्से में मैं ग़लत हूँक्या अभी किसी और के लिए भी लिंक टूट गया?
संपादन: blog post लगभग आधे सेकंड तक दिखती है, फिर 404 page पर चली जाती है
संपादन 2: मैं ब्लॉग पोस्ट सूची में गया, इधर-उधर क्लिक किया, और सूची में मौजूद उसी लेख को खोलने पर भी 404 page मिला। static page, या कम से कम static होना चाहिए ऐसे ब्लॉग को कोई इस हद तक कैसे बिगाड़ सकता है?
वैसे मैंने भी शायद वही reproduction steps आज़माए, लेकिन मुझे एक बार भी 404 नहीं मिला। मैंने phone और desktop दोनों पर, JavaScript on और off दोनों हालत में कोशिश की। इसलिए लगता है कि जो व्यवहार देखा गया वह दिखने से ज़्यादा जटिल रहा होगा।