1 पॉइंट द्वारा GN⁺ 2026-05-06 | 2 टिप्पणियां | WhatsApp पर शेयर करें
  • Async Rust executor-स्वतंत्र कोड को server और microcontroller पर साथ चलाने देता है, लेकिन compiler द्वारा बनाए गए state machine की वजह से खासकर embedded में binary size बढ़ना साफ़ दिखता है
  • bar() जैसे साधारण उदाहरण में भी, जहाँ 2 await points हैं, 360 lines of MIR और Unresumed, Returned, Panicked, Suspend0, Suspend1 states बनते हैं, जबकि 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_resume pass आख़िरी async-विशेष MIR pass है
    • async MIR में मौजूद रहता है, लेकिन LLVM IR में नहीं, इसलिए async का state machine में रूपांतरण MIR pass में होता है
  • bar function 360 lines of MIR बनाता है, जबकि synchronous version सिर्फ 23 lines इस्तेमाल करता है
  • compiler द्वारा output किया गया CoroutineLayout असल में enum-जैसे state set के बराबर है
    • Unresumed: शुरुआती state
    • Returned: completed state
    • Panicked: panic के बाद की state
    • Suspend0: पहला await point, जहाँ foo future स्टोर होता है
    • Suspend1: दूसरा await point, जहाँ पहला result और दूसरा foo future स्टोर होता है
  • Future::poll एक safe function है, इसलिए future पूरा होने के बाद भी अगर उसे फिर बुलाया जाए तो वह UB नहीं पैदा करना चाहिए
    • अभी Suspend1 के बाद यह Ready लौटाता है और future को Returned state में बदल देता है
    • इस state में दोबारा poll करने पर panic होता है
  • Panicked state शायद इसीलिए है कि async function panic होने के बाद, अगर उसे catch_unwind से पकड़ा जाए, तो उस future को फिर poll न किया जा सके
    • panic के बाद future अधूरी state में हो सकता है, इसलिए दोबारा poll करना UB तक ले जा सकता है
    • यह mechanism mutex poisoning से काफ़ी मिलता-जुलता है
    • Panicked state की यह व्याख्या पूरी तरह दस्तावेज़ित नहीं मिली, इसलिए इस पर लगभग 90% भरोसा बताया गया है

completion के बाद poll पर क्या panic ज़रूरी है?

  • Returned state का future अभी panic करता है, लेकिन यह अनिवार्य नहीं है
    • ज़रूरी शर्त सिर्फ यह है कि UB न हो
  • panic अपेक्षाकृत महँगा है, और ऐसा path जोड़ता है जिसमें side effects होते हैं जिन्हें optimize करके हटाना कठिन है
  • completed future को फिर poll करने पर Poll::Pending लौटाने से unsafe behavior के बिना Future type का 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 उपयोग करने पर Panicked state को ही हटाया जा सकने की संभावना है, लेकिन उसके असर की और जाँच चाहिए

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 completion assert से 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 यह जानता है कि foo 5 लौटाता है, लेकिन bar के answer को 10 तक optimize नहीं कर पाता
    • foo के poll function call भी बचे रहते हैं
    • वजह वे संभावित panic paths हैं जिन्हें compiler पूरी तरह समझ नहीं पाता
    • LLVM यह नहीं जानता कि foo वास्तव में सिर्फ एक बार call होगा और panic नहीं करेगा
  • 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 बनाता है और उसके भीतर foo state machine को call करता है
    • ज़्यादा कुशल रूप में bar खुद foo future बन सकता है
  • 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 नहीं बन सकता, लेकिन अपनी ज़्यादातर state foo पर निर्भर कर सकता है
  • manual implementation में BarFut के पास Unresumed { input } और Inlined { foo: FooFut } states हो सकती हैं
    • पहले poll पर preamble चलाकर foo(blah) बनाया जाता है और state को Inlined में बदला जाता है
    • उसके बाद foo.poll(cx) के result पर postamble लागू किया जाता है
  • अगर पहले await point तक का code पहले से चलाया जा सके, तो Unresumed state भी हटाई जा सकती है, लेकिन यह 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).await
    • CommandId::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 के लिए 123
    • CommandId::B के लिए 456
    • उसके बाद send_response(response).await
  • refactor के बाद CoroutineLayout में सिर्फ एक stored future बचता है और सिर्फ एक Suspend0 state रहती है
  • कुल MIR length घटकर 302 lines रह जाती है और duplication हट जाती है
  • इसलिए ऐसे optimization pass उपयोगी लगते हैं जो identical code paths और states को पहचानकर एक में fold कर सकें
    • यह optimization future inlining pass के साथ अच्छी तरह जुड़ सकती है

प्रयोग लिंक और अतिरिक्त benchmark

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 टिप्पणियां

 
GN⁺ 2026-05-06
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 मजबूरी बन जाए। ऐसी केंद्रीय निर्भरता स्वस्थ नहीं है

    • संदर्भ के हिसाब से पूरा ecosystem tokio पर निर्भर दिख सकता है, लेकिन embedded Rust को देखें तो बात कुछ ज़्यादा समझ में आती है
      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 इस्तेमाल करके संतुष्ट हूँ, लेकिन दूसरे लोग smol, async-std, glommio जैसे दूसरे executors इस्तेमाल करें तो वह भी अच्छा है
      tokio standard library का हिस्सा नहीं है, फिर भी अच्छी तरह maintain हो रहा है, इसलिए अभी की स्थिति ठीक लगती है। उल्टा, अगर यह standard library में चला जाए तो दूसरे executors इस्तेमाल करना कठिन हो सकता है, और standard library को दूसरे platforms पर port करना भी मुश्किल हो सकता है
      बेशक, हो सकता है यह चिंता निराधार हो
    • Java का ज़िक्र दिलचस्प है, क्योंकि Java ने भी ऐतिहासिक रूप से ऐसी ही समस्याएँ झेली हैं
      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 की भरमार से बचा जा सके
    • async इस्तेमाल करते हुए भी tokio पर निर्भर हुए बिना Rust को इस्तेमाल करने के बहुत से क्षेत्र हैं। सच कहूँ तो पूरी तरह tokio से बँधा हुआ क्षेत्र web/server पक्ष के ज़्यादा करीब लगता है
      libraries को executor-independent तरीके से लिखना बहुत कठिन नहीं है, लेकिन लगातार सावधानी चाहिए, और community के ज़्यादातर हिस्सों में यह हमेशा पालन नहीं होता
  • शानदार लेख है। मुझे ऐसे optimization deep dive बहुत पसंद हैं, और उम्मीद है कि project goals भी अच्छी तरह पूरे हों
    मुझे अक्सर लगा है कि compiler “तुच्छ” मामलों के optimization पर बहुत बड़ा प्रयास नहीं लगाता
    लेकिन शीर्षक, सामग्री की तुलना में, बहुत नाटकीय है। “Async Rust Optimizations the Compiler Still Misses” भी होता तो मैं क्लिक करता

    • शीर्षक बस इसलिए चुना गया क्योंकि वह तथ्यात्मक है। 2019 के आसपास async आने के बाद से बहुत कुछ बदला नहीं है
      अब 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 है

    • “सामान्य code भी पहले से async है, और इंतज़ार के समय thread सो जाता है, kernel उसे abstract कर देता है” — यह बात सटीक नहीं है
      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” नहीं देता, इसलिए इसका पूरा लाभ उठाना कुछ कठिन हो जाता है
    • जैसे ही kernel और OS scheduler बीच में आते हैं, चीज़ें अपनी संभावित गति से 3–4 orders of magnitude तक धीमी हो सकती हैं
      पिछली बार जब मैंने coroutine/scheduling code के साथ काम किया था, तब तुरंत समाप्त होने वाला thread बनाकर join करने में लगभग 200µs लगे थे, जबकि अपनी green thread बनाकर schedule करके उसका इंतज़ार करने में लगभग 400ns लगे
      किसी के फिर से absurd रूप से जटिल async framework डिज़ाइन करने के लिए 10 साल इंतज़ार करने की ज़रूरत नहीं है। किसी भी systems language में assembly की 20 lines से आप खुद green threads/stackful coroutines बना सकते हैं
    • “threads सही programming model हैं” या नहीं, यह इस पर निर्भर करता है कि आप क्या कर रहे हैं। computation-heavy work के लिए threads सही हैं, और bandwidth-heavy work के लिए async सही है
      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 करना आसान बनाने के लिए मौजूद है
    • मुझे तो callbacks पर reasoning करना उल्टा ज़्यादा आसान लगता है
      जब 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 को मिलाकर दोनों के नुकसान भी साथ पाए जा सकते हैं
    • threads, async+callbacks से बेहतर या बदतर नहीं हैं, बल्कि अलग model हैं। कुछ समस्याएँ threads के लिए उपयुक्त हैं, और कुछ समस्याएँ async में व्यक्त करना कहीं बेहतर है
  • अगर 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 जोड़ती है

    • Rust-in-Linux पक्ष इस समस्या को fallible memory operations जैसी चीज़ों से संभाल रहा है। उनके लिए यह ज़रूरी feature है
      proof-driven usage बढ़ाने का काम भी धीरे-धीरे चल रहा है, जिसमें array के खाली न होने जैसी बातों को साबित करना शामिल है
    • panic usability और safety, दोनों के लिए काफ़ी महत्वपूर्ण है
      अगर panic न हो और हर स्थिति में execution जारी रखना पड़े, तो ऐसी memory corruption जैसी स्थितियों में recovery के लिए बहुत-सी error handling डालनी पड़ेगी जहाँ invariants टूट चुके हों, और हर जगह invariants जाँचने पड़ेंगे
      यह ठीक उसी तरह की समस्या है जिसकी आप चिंता कर रहे हैं: ऐसी स्थितियों के लिए विशाल error handling जो लगभग कभी घटित नहीं होंगी
    • Rust का लक्ष्य memory safety है। panic, memory safety के नज़रिए से पूरी तरह सुरक्षित है
    • जिस OS पर आप program चला रहे हैं, वह भी परिपूर्ण नहीं है
      इस रवैये से थकान होती है कि 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 देखे, लेकिन सबमें कोई न कोई समस्या या कड़ी सीमाएँ थीं

    • keyword generics पर काम चल रहा है, जिससे 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
    • यह बहुत हद तक इस पर निर्भर करता है कि आप वास्तव में क्या कर रहे हैं, लेकिन अगर चीज़ें काफ़ी सरल हों, तो शायद ऐसा macro बनाया जा सकता है जो types और await को बदल-बदलकर लगा दे
    • यह क्लासिक function color problem है। https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • मेरे नज़रिए से async function पहले से ही 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” शब्द वास्तविक अर्थ की तुलना में काफ़ी भ्रामक है
      project goals एक ऐसा सिस्टम है जिसमें कोई व्यक्ति या छोटा समूह यह जताता है कि वह कोई काम करना चाहता है, और Rust project volunteers से code review, सवालों के जवाब जैसी निरंतर सहायता का समय माँगता है
      इसका मतलब यह नहीं कि Rust project ने खुद वह goal तय किया है, या उसका अनिवार्य रूप से समर्थन किया है
      इसलिए इसे Rust का आधिकारिक roadmap मानना सही नहीं होगा; ज़्यादा सही यह कहना है कि “इस क्षेत्र में काम करना चाहने वाले contributors मौजूद हैं”
    • C++ ISO committee के भीतर भी लगता है कि language के evolution process के कुछ हद तक बिगड़ जाने पर सहमति है। मुख्य कारण उसका आकार और संगठन का तरीका है
      जब technology व्यावसायिक रूप से स्थापित हो जाती है, तो दुर्भाग्य से चीज़ें अक्सर ऐसे ही बहती हैं। बड़े sponsors को इस बात के लिए दोष देना कठिन है कि वे सिर्फ अपने रुचि-क्षेत्रों को fund करते हैं
      ख़ुशी की बात है कि TweedeGolf की काफ़ी funding, मेरी जानकारी में, Netherlands government से आती है
    • open source काम के लगभग दो प्रकार लगते हैं: feature development और maintenance
      नई 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 चाहिए

    • कम से कम ऐसा custom lint तो चाहिए जो बता सके कि यह कहाँ लागू हो सकता है। उस बिंदु पर बात काफ़ी हद तक compiler work के करीब पहुँच जाती है
  • “Future आसानी से inline नहीं होते” वाली बात पर, मैंने अपनी बनाई programming language में async function के भीतर async function calls को inline करने के लिए एक custom pass लिखा था
    वह आम तौर पर अच्छी तरह काम करता है और कुछ boilerplate हटा सकता है, लेकिन परिणामस्वरूप binary size काफ़ी बढ़ जाती है
    तकनीकी रूप से Rust भी वही काम कर सकता है

 
GN⁺ 2026-05-06
Lobste.rs की राय
  • सिर्फ़ शीर्षक देखकर जितना सोचा था, उससे कहीं ज़्यादा रचनात्मक लेख था

    • मुझे तो यह लगभग तथ्य के क़रीब लगता है। MVP रिलीज़ के 7 साल बाद भी language design या compiler implementation में लगभग कोई प्रगति नहीं हुई, और जिन लोगों ने मुख्य रूप से MVP बनाया था उन्होंने भी लगभग उसी समय प्रोजेक्ट में अपनी भागीदारी कम कर दी, इसलिए उसके बाद प्रगति रुक-सी गई।
      उम्मीद है कि जो लोग इस पर काम करना चाहते हैं, उन्हें ज़रूरी support मिले
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    अच्छा लगा कि इस समस्या पर काम हो रहा है। मैंने कई बार ऐसे लेख देखे हैं जिनमें कहा गया है कि अभी rustc LLVM को बहुत ज़्यादा code सौंप देता है और उम्मीद करता है कि optimizer सब संभाल लेगा, और खासकर यह लेख इस काम के लिए funding भी माँग रहा है

  • हे भगवान, मैं कितना मूर्ख था
    मैं हमेशा सोचता था कि async किसी न किसी रूप में runtime, task tracking, और completion की जाँच के लिए polling माँगता है, इसलिए वह मूल रूप से “भारी” ही होगा। क्योंकि वह overhead शून्य तो नहीं है
    मैं समझता था कि यहाँ “zero-cost abstraction” language feature की बात है, जो जोड़े गए runtime से अलग है
    LLVM को देने से पहले rustc वास्तव में क्या emit करता है, यह देखने का ख़याल ही कभी नहीं आया

  • जो लोग async Rust से परिचित नहीं हैं, उनके लिए:

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    यह बात सच में सही है। async calls की nested tree भी अगर पूरी optimization से गुज़रे तो आख़िर में अंदर state machine रखने वाले एक single struct में सिमट जाती है। यह वाक़ई बहुत चतुर तरीका है

  • अगर release build में यह case आ जाए, तो क्या इससे किसी तरह का deadlock बनता है? या फिर हमेशा Pending रहने वाले कामों का इंतज़ार कर रहे tasks की वजह से leak भी हो सकता है?

    • हाँ। ऐसे future रुकी हुई अवस्था में चले जाते हैं और कभी पूरा नहीं होते। लेकिन ऐसी स्थिति तक पहुँचना आम तौर पर सिर्फ़ buggy low-level async code में ही संभव होता है, और जो code completed future को ठीक से track नहीं कर पाता वह शायद पहले से ही leak और deadlock पैदा कर रहा होगा
      .await के साथ गलत polling नहीं की जा सकती
  • कुछ विचार आए:

    1. यह लेख मानो यह कह रहा है कि और optimization logic को LLVM के बाहर निकालकर MIR layer में लाना चाहिए। उदाहरण के लिए, यह समझ आता है कि async function inlining, LLVM की तुलना में MIR में आसान है। अगर async के लिए यह MIR में किया जा सकता है, तो क्या उस logic को synchronous functions पर भी generalize किया जा सकता है और LLVM के कुछ optimization passes हटाए जा सकते हैं? मुझे पता है यह बहुत बड़ा काम है, और यह व्यावहारिक सवाल से ज़्यादा दिशा से जुड़ा विचार है। शायद जब frontend/middle-end compiler एक स्तर की जटिलता तक पहुँच जाए, तब LLVM की काफ़ी generic optimizations को कहीं और ले जाना बेहतर हो
    2. मुझे अब भी 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 होना चाहिए ऐसे ब्लॉग को कोई इस हद तक कैसे बिगाड़ सकता है?

    • लहजा थोड़ा बेवजह रूखा और आक्रामक लग रहा है। वेबसाइटों में भी bug हो सकते हैं, और उसकी रिपोर्ट करना उपयोगी है, लेकिन यह टिप्पणी कुछ ज़्यादा ही चिड़चिड़ी लगती है
      वैसे मैंने भी शायद वही reproduction steps आज़माए, लेकिन मुझे एक बार भी 404 नहीं मिला। मैंने phone और desktop दोनों पर, JavaScript on और off दोनों हालत में कोशिश की। इसलिए लगता है कि जो व्यवहार देखा गया वह दिखने से ज़्यादा जटिल रहा होगा।