Async कोड धीमा होने के कारण और समाधान
(secondb.ai)Async कोड धीमा होने के कारण और समाधान (तकनीकी सारांश)
यह वीडियो उन सामान्य कारणों पर चर्चा करता है जिनकी वजह से Python का asyncio कोड synchronous कोड से भी धीमा हो जाता है, और उन्हें ठीक करने के तकनीकी तरीकों को समझाता है।
1. Asyncio के मुख्य कॉन्सेप्ट
- इवेंट लूप (Event Loop): यह हर asynchronous application का core है। इसे
asyncio.run()से शुरू किया जाता है, और यह single thread में task execution को manage और schedule करता है। - Coroutine:
async defसे घोषित asynchronous function।awaitkeyword मिलने पर यह 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 withsyntax से 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:
asyncioapplications के लिए 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 ज़्यादा होता है।
12 टिप्पणियां
Python वाकई एक बेहतरीन भाषा है, लेकिन इसका asynchronous interface शायद गलत तरीके से डिज़ाइन किया गया फ़ीचर लगता है।
नंबर 4 में
eager_start=Trueछूट गया है।create_taskweakref बनाता है, इसलिए कोड ऐसा टास्क बन सकता है जो हमेशा के लिए कभी execute ही न हो....लगता है यह शख्स भी Python async की वजह से Node.js पर चला गया था
निष्कर्ष: Python का asynchronous interface अभी भी सहज नहीं है।
असल में, अगर प्रोजेक्ट Python async को optimize करने लायक बड़ा है, तो उसे किसी दूसरी language में लिखना performance और stability—दोनों के लिहाज़ से कहीं बेहतर होता है।
अगर compiled language पर नहीं जा रहे हैं, तो क्या performance में बहुत बड़ा फ़र्क पड़ता है? Multi-threading हो तो GIL की मौजूदगी की वजह से बड़ा अंतर आएगा, लेकिन अगर वैसे भी event loop पर चलने वाली async संरचना है, तो भाषा के हिसाब से किस तरह का फ़र्क आता है, यह जानने की जिज्ञासा है।
jit compileका होना या न होना सोच से कहीं ज़्यादा असर डालता है। V8 काफ़ी अच्छी तरह optimize किया गया है।मैंने 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 भी फूट जाता था, ऐसा दुर्लभ अनुभव किया था hahaAsync/Await पैटर्न की मूल प्रेरणा(?) मानी जा सकने वाली C# इस्तेमाल करने वाली कंपनी में काम करने वाले व्यक्ति के नज़रिए से भी देखें, तो 1 नंबर की गलती की तरह
awaitको बस सीधे क्रम से लगातार लिख देने वाला गलत कोड काफ़ी बार दिख जाता है.ऐसा कोड देखकर आम तौर पर यह महसूस होता है कि लोगों को बस इतना पता है कि
asyncmethod call के आगेawaitkeyword लगाना है, लेकिन उससे आगे asynchronous execution order के बारे में वे ज़्यादा नहीं सोचते, इसलिए ऐसा कोड निकलता है.जब कई
awaitआते हैं, तब किसी का result ठीक नीचे ही इस्तेमाल होना है, इसलिए उसके पहलेTask<T>object केawaitresult value को ले लेना, और किसी का इस्तेमाल काफ़ी बाद में होना है, इसलिए पहले सिर्फTask<T>लेकर बाद मेंawaitकरना—इस तरह async flow को ध्यान में रखकर कोड लिखना उतना ही दिमाग़ लगाने वाला काम है.कम से कम मैं तो async घोषित किए गए method में processing flow को ध्यान में रखकर ही कोड लिखता हूँ, लेकिन कभी-कभी maintenance कर रहे किसी पूर्व कर्मचारी का छोड़ा हुआ कोड देखते समय ऐसा भी लगता है, ‘मैं तो बस सिंपल synchronous code लिखना चाहता हूँ, लेकिन बीच में इस्तेमाल करने वाला method सिर्फ async type में ही है, इसलिए मैं बस ऐसे ही लिख देता हूँ.’
अगर 1 नंबर वाला हिस्सा हमेशा स्वतंत्र है, तो उसे उस तरह करना अच्छा है,
लेकिन बाद में कोड बदलने पर अगर वह स्वतंत्र न रहे, तो उस फ़ंक्शन का इस्तेमाल करने वाली सभी जगहों की जाँच करके उन्हें बदलना पड़ेगा — यह भी काफ़ी असुविधाजनक लगता है।
अगर काम में बहुत ज़्यादा समय नहीं लगता, तो कोड मैनेजमेंट के लिहाज़ से
awaitको सीरियल में करना शायद बेहतर हो सकता हैमुझे लगता है कि इसे इस विचार के साथ समझना चाहिए कि 'multithreading में overhead बोझिल होता है, इसलिए उसके विकल्प के रूप में single thread को बाँटकर parallel processing की समस्या हल की जाती है।' इसी वजह से, मूल रूप से multithreading की तुलना में कुछ स्थितियों में इस पर और भी ज़्यादा ध्यान देना सही लगता है।
वह बात भी सही है।
लगता है कि सही मायने में अच्छी asynchronous code मूल रूप से ऐसा code है, जिस पर बहुत ध्यान देना पड़ता है।