23 पॉइंट द्वारा darjeeling 2025-11-16 | 12 टिप्पणियां | WhatsApp पर शेयर करें

Async कोड धीमा होने के कारण और समाधान (तकनीकी सारांश)

यह वीडियो उन सामान्य कारणों पर चर्चा करता है जिनकी वजह से Python का asyncio कोड synchronous कोड से भी धीमा हो जाता है, और उन्हें ठीक करने के तकनीकी तरीकों को समझाता है।

1. Asyncio के मुख्य कॉन्सेप्ट

  • इवेंट लूप (Event Loop): यह हर asynchronous application का core है। इसे asyncio.run() से शुरू किया जाता है, और यह single thread में task execution को manage और schedule करता है।
  • Coroutine: async def से घोषित asynchronous function। await keyword मिलने पर यह execution को अस्थायी रूप से रोककर control वापस event loop को दे सकता है।
  • Task: coroutine को wrap करके event loop में एक साथ चलने के लिए schedule करता है। इसे asyncio.create_task() से बनाया जाता है।
  • Future: asynchronous operation के final result को दर्शाने वाला low-level object।

2. Synchronous कोड को asynchronous में बदलने का उदाहरण

मौजूदा synchronous time.sleep() को asynchronous await asyncio.sleep() से बदला जाता है, function को async def से घोषित किया जाता है, और main coroutine को asyncio.run() से चलाया जाता है।


परफॉर्मेंस गिराने वाली आम गलतियाँ और उनके समाधान

गलती 1: क्रमवार execution (Sequential Execution)

अगर independent tasks को parallel में चलाने के बजाय क्रमवार await किया जाए, तो कुल execution time सभी tasks के समय का योग बन जाता है।

  • गलत उदाहरण (क्रमवार):

    # हर await पिछले काम के खत्म होने तक इंतज़ार करता है  
    await get_user_notifications()  
    await get_recent_activity()  
    await get_unread_messages()  
    
  • समाधान (parallel): asyncio.gather या asyncio.TaskGroup का उपयोग करके independent tasks को एक साथ चलाएँ। इससे कुल execution time सबसे ज़्यादा समय लेने वाले task के बराबर रह जाता है।

    # तीनों काम एक साथ शुरू होते हैं  
    await asyncio.gather(  
        get_user_notifications(),  
        get_recent_activity(),  
        get_unread_messages()  
    )  
    

Parallel execution tools की तुलना

  • asyncio.gather:
    • कई coroutines को एक साथ चलाता है।
    • नुकसान: error handling सीमित है। अगर एक task में exception आता है, तो बाकी चल रहे tasks cancel हो जाते हैं।
  • asyncio.create_task:
    • हर task पर अलग control और error handling की सुविधा देता है।
    • background execution के लिए उपयोगी है, लेकिन कई tasks को अलग-अलग await करना पड़ता है।
  • asyncio.TaskGroup (Python 3.11+):
    • 'structured concurrency' के लिए आधुनिक विकल्प।
    • async with syntax से task group को manage करता है, और context से बाहर निकलते समय सभी tasks के complete होने या exception handling की गारंटी देता है।
    async with asyncio.TaskGroup() as tg:  
        tg.create_task(some_coro_1())  
        tg.create_task(some_coro_2())  
    # 'async with' ब्लॉक खत्म होते ही सभी tasks await हो जाते हैं  
    

गलती 2: synchronous libraries का उपयोग

अगर asyncio कोड के अंदर requests या pathlib जैसी synchronous (blocking) libraries का उपयोग किया जाए, तो पूरा event loop block हो जाता है। asyncio.gather के अंदर उपयोग करने पर भी यह व्यवहार में क्रमवार ही चलता है।

  • समाधान: aiohttp (requests का विकल्प), aiofiles (files/pathlib का विकल्प) जैसी dedicated libraries का उपयोग करें जो asynchronous (non-blocking) support देती हैं।

गलती 3: CPU-bound काम से event loop block होना

asyncio single thread में चलता है, इसलिए भारी computation जैसे CPU-bound काम event loop को रोक देते हैं और दूसरे I/O operations को delay करते हैं।

  • समाधान: loop.run_in_executor() का उपयोग करके CPU-bound काम को अलग thread pool (default) या process pool में offload करें।
    loop = asyncio.get_running_loop()  
    # CPU-intensive function को अलग thread में चलाएँ  
    await loop.run_in_executor(  
        None,  # default thread pool का उपयोग  
        cpu_bound_function,  
        arg1  
    )  
    

गलती 4: गैर-ज़रूरी काम की वजह से blocking

logging जैसे non-core काम, जिनका user response से सीधा संबंध नहीं है, अगर await किए जाएँ तो response time बेवजह बढ़ जाता है।

  • समाधान: asyncio.create_task() से ऐसे काम को background task में अलग करें और उसे await न करें।
    user_profile = await get_user_profile()  
    # logging को await किए बिना background में चलाएँ  
    asyncio.create_task(send_logs_to_external_service())  
    return user_profile  
    

गलती 5: बहुत ज़्यादा tasks बनाना

बहुत छोटे-छोटे कामों को बड़ी संख्या में task में बदलने से context switching overhead बढ़ सकता है और performance गिर सकती है।

  • समाधान 1: छोटे कामों को batching करके कुछ बड़े tasks में बदलें।
  • समाधान 2: asyncio.Semaphore का उपयोग करके एक साथ चलने वाले tasks की अधिकतम संख्या सीमित करें।
    # एक समय में अधिकतम 10 tasks की अनुमति  
    semaphore = asyncio.Semaphore(10)  
    
    async with semaphore:  
        await fetch_data()  
    

अन्य गलतियाँ

  • "Never Awaited" coroutines: coroutine को कॉल करके await न करने पर काम चलता ही नहीं और चुपचाप fail हो सकता है। इसे flake8-async जैसे linter से पकड़ा जा सकता है।
  • गलत resource management: files, DB connections आदि को try...finally के बिना उपयोग करने पर resource leak हो सकता है। इसका समाधान async with वाले asynchronous context manager से किया जा सकता है।

डिबगिंग और concurrency model का चयन

Asyncio debug mode

डिफ़ॉल्ट रूप से बंद debug mode को enable (asyncio.run(debug=True)) करने पर यह नीचे जैसी समस्याएँ पकड़ने में मदद करता है।

  • await न किए गए coroutines (RuntimeWarning)।
  • गलत thread से कॉल किए गए asynchronous API।
  • 100ms से ज़्यादा समय लेने वाले callbacks।
  • धीमे I/O selector operations।

अन्य debugging tools

  • Scalene: CPU और memory profiler।
  • aio-monitor: asyncio applications के लिए monitoring और CLI।
  • pdb: Python का built-in debugger।
  • py-stack: चल रहे Python process का stack trace प्रिंट करके blocking points का पता लगाता है।

Concurrency model चुनने की गाइड

  • Asyncio (single thread): ज़्यादा latency वाले बड़े पैमाने के I/O-bound कामों (जैसे network requests, file I/O) के लिए सबसे उपयुक्त।
  • Threads (multi-thread): ऐसे I/O-bound कामों के लिए जहाँ shared data access की ज़रूरत हो। GIL (Global Interpreter Lock) की वजह से यह सच्चा parallel execution नहीं देता, लेकिन I/O wait के दौरान दूसरा thread चल सकता है।
  • Processes (multi-process): CPU-bound कामों (जैसे image processing, heavy computation) के लिए। यह कई CPU cores का उपयोग करके वास्तविक parallelism देता है, लेकिन memory और communication overhead ज़्यादा होता है।

https://youtu.be/wGDOwNW6lVk

12 टिप्पणियां

 
savvykang 2025-11-18

Python वाकई एक बेहतरीन भाषा है, लेकिन इसका asynchronous interface शायद गलत तरीके से डिज़ाइन किया गया फ़ीचर लगता है।

 
ceruns 2025-11-17

नंबर 4 में eager_start=True छूट गया है। create_task weakref बनाता है, इसलिए कोड ऐसा टास्क बन सकता है जो हमेशा के लिए कभी execute ही न हो....

 
tested 2025-11-17

https://rosettalens.com/s/ko/python-to-node

लगता है यह शख्स भी Python async की वजह से Node.js पर चला गया था

 
kandk 2025-11-17

निष्कर्ष: Python का asynchronous interface अभी भी सहज नहीं है।

 
bungker 2025-11-17

असल में, अगर प्रोजेक्ट Python async को optimize करने लायक बड़ा है, तो उसे किसी दूसरी language में लिखना performance और stability—दोनों के लिहाज़ से कहीं बेहतर होता है।

 
euphcat 2025-11-17

अगर compiled language पर नहीं जा रहे हैं, तो क्या performance में बहुत बड़ा फ़र्क पड़ता है? Multi-threading हो तो GIL की मौजूदगी की वजह से बड़ा अंतर आएगा, लेकिन अगर वैसे भी event loop पर चलने वाली async संरचना है, तो भाषा के हिसाब से किस तरह का फ़र्क आता है, यह जानने की जिज्ञासा है।

 
vwjdalsgkv 2025-11-17

jit compile का होना या न होना सोच से कहीं ज़्यादा असर डालता है। V8 काफ़ी अच्छी तरह optimize किया गया है।

 
euphcat 2025-11-16

मैंने source वीडियो नहीं देखा है, लेकिन गलती 4 के लिए दिया गया समाधान वाला कोड गलत है.

create_task() से लौटने वाला task instance कम-से-कम एक variable में assign होना चाहिए, और वह variable task के खत्म होने तक ज़िंदा रहना चाहिए. नहीं तो coroutine के चलने के दौरान task instance के garbage collect हो जाने का जोखिम रहता है.

अगर ऊपर की तरह task बनाने वाला function तुरंत खत्म होने वाला हो, तो task instance को return करना, global variable में assign करना, या instance variable में assign करना जैसे तरीके इस्तेमाल करने चाहिए.

P.S.)
भले ही return value की खास ज़रूरत न हो, और आपको पूरा यक़ीन हो कि coroutine थोड़े समय में खत्म हो जाएगा, फिर भी task instance पर कभी-न-कभी await लगने वाली तरह से code लिखना बेहतर है. अगर वह पसंद न हो, तो task की तरह चलने वाले हर coroutine में कड़ा exception handling लगाकर ऐसा structure बनाना चाहिए कि log message बिना किसी चूक के दिखे. नहीं तो task चाहे कितनी भी बड़ी गड़बड़ी कर दे, ऐसी स्थिति आ सकती है जहाँ Exception handle नहीं होता और silently fail हो जाता है.

जिस project को मैं रोज़गार के तौर पर develop/manage करता हूँ, उसमें मैंने ऐसा pattern design किया था जहाँ दर्जनों modules अपने-अपने while self.ok(): cmd = await self.cmd_queue.get(); await self.process(cmd); जैसे task एक-एक बनाकर लगातार चलाते थे. Exception handling pattern ठीक से स्थापित करने से पहले, हर बार कोई एक problem फूटती थी तो उसके साथ मेरा mental भी फूट जाता था, ऐसा दुर्लभ अनुभव किया था haha

 
kunggom 2025-11-16

Async/Await पैटर्न की मूल प्रेरणा(?) मानी जा सकने वाली C# इस्तेमाल करने वाली कंपनी में काम करने वाले व्यक्ति के नज़रिए से भी देखें, तो 1 नंबर की गलती की तरह await को बस सीधे क्रम से लगातार लिख देने वाला गलत कोड काफ़ी बार दिख जाता है.

ऐसा कोड देखकर आम तौर पर यह महसूस होता है कि लोगों को बस इतना पता है कि async method call के आगे await keyword लगाना है, लेकिन उससे आगे asynchronous execution order के बारे में वे ज़्यादा नहीं सोचते, इसलिए ऐसा कोड निकलता है.
जब कई await आते हैं, तब किसी का result ठीक नीचे ही इस्तेमाल होना है, इसलिए उसके पहले Task<T> object के await result value को ले लेना, और किसी का इस्तेमाल काफ़ी बाद में होना है, इसलिए पहले सिर्फ Task<T> लेकर बाद में await करना—इस तरह async flow को ध्यान में रखकर कोड लिखना उतना ही दिमाग़ लगाने वाला काम है.

कम से कम मैं तो async घोषित किए गए method में processing flow को ध्यान में रखकर ही कोड लिखता हूँ, लेकिन कभी-कभी maintenance कर रहे किसी पूर्व कर्मचारी का छोड़ा हुआ कोड देखते समय ऐसा भी लगता है, ‘मैं तो बस सिंपल synchronous code लिखना चाहता हूँ, लेकिन बीच में इस्तेमाल करने वाला method सिर्फ async type में ही है, इसलिए मैं बस ऐसे ही लिख देता हूँ.’

 
skageektp 2025-11-17

अगर 1 नंबर वाला हिस्सा हमेशा स्वतंत्र है, तो उसे उस तरह करना अच्छा है,
लेकिन बाद में कोड बदलने पर अगर वह स्वतंत्र न रहे, तो उस फ़ंक्शन का इस्तेमाल करने वाली सभी जगहों की जाँच करके उन्हें बदलना पड़ेगा — यह भी काफ़ी असुविधाजनक लगता है।
अगर काम में बहुत ज़्यादा समय नहीं लगता, तो कोड मैनेजमेंट के लिहाज़ से await को सीरियल में करना शायद बेहतर हो सकता है

 
euphcat 2025-11-17

मुझे लगता है कि इसे इस विचार के साथ समझना चाहिए कि 'multithreading में overhead बोझिल होता है, इसलिए उसके विकल्प के रूप में single thread को बाँटकर parallel processing की समस्या हल की जाती है।' इसी वजह से, मूल रूप से multithreading की तुलना में कुछ स्थितियों में इस पर और भी ज़्यादा ध्यान देना सही लगता है।

 
kunggom 2025-11-17

वह बात भी सही है।
लगता है कि सही मायने में अच्छी asynchronous code मूल रूप से ऐसा code है, जिस पर बहुत ध्यान देना पड़ता है।