JavaScript से Python पर स्विच करना ऐसा लगता है जैसे किसी ऐसे शहर में जाना जहाँ सड़क के संकेत तो वही हैं लेकिन ट्रैफिक के नियम अलग हैं। सिंटैक्स दोस्ताना और परिचित लगता है। आप व्याकरण में async और await को वहीं देखते हैं, इसलिए आप मान लेते हैं कि इसका मेंटल मॉडल (mental model) आसानी से काम करेगा। लेकिन ऐसा नहीं है। JavaScript का एक आदत भरा पैटर्न आपके Python परफॉरमेंस को बिना क्रैश किए, बिना कोई एरर लॉग किए और बिना किसी क्विक कोड रिव्यू में दिखे, चुपचाप खराब कर देता है।
JavaScript आपको कैसे "शुरू करो और भूल जाओ" सिखाता है
JavaScript में, एक async फंक्शन कॉल 'eager' (तत्पर) होता है। जैसे ही आप इसे कॉल करते हैं, इंजन एक Promise बनाता है और काम तुरंत शुरू हो जाता है। इवेंट लूप पहले से ही दौड़ में लग चुका होता है। इसीलिए JavaScript डेवलपर्स स्वाभाविक रूप से इस तरह कोड लिखते हैं:
const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;
किसी भी await तक पहुँचने से पहले ही दोनों नेटवर्क रिक्वेस्ट चल रही होती हैं। पहला await वर्तमान फंक्शन को तब तक रोक देता है जब तक fetchUser रिज़ॉल्व (resolve) नहीं हो जाता, लेकिन fetchOrders पिछली लाइन से ही बैकग्राउंड में चलता आ रहा है। जब तक आपको orders वेरिएबल की ज़रूरत पड़ती है, दूसरी रिक्वेस्ट शायद पहले ही पूरी हो चुकी होती है। JavaScript में यह पैटर्न इतना स्वाभाविक लगता है कि कई डेवलपर्स इसे कॉनकरेंसी (concurrency) का तरीका भी नहीं मानते। यह बस async के काम करने का तरीका है।
Python का सरप्राइज: एक कोल्ड कोरूटीन (Cold Coroutine)
Python एक अलग कॉन्ट्रैक्ट का उपयोग करता है। जब आप Python में async def फंक्शन को कॉल करते हैं, तो आप कोई काम शुरू नहीं करते। आपको एक coroutine ऑब्जेक्ट मिलता है। इसे कागज पर लिखी एक रेसिपी की तरह समझें। सामग्री सूचीबद्ध है, चरण स्पष्ट हैं, लेकिन ओवन में कुछ भी नहीं है। जब तक कोई चीज़ स्पष्ट रूप से उस coroutine को इवेंट लूप के माध्यम से नहीं चलाती, तब तक वह निष्क्रिय (inert) रहता है।
यहाँ एक जाल है। एक JavaScript इंजीनियर जिसे यूजर और उसके ऑर्डर्स की ज़रूरत है, वह Python में ऐसा लिख सकता है:
user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro
यह कॉनकरेंट (concurrent) दिखता है। यह कॉनकरेंट जैसा महसूस होता है। लेकिन यह पूरी तरह से सीक्वेंशियल (sequential) है।
पहली लाइन user_coro को एक सुप्त (dormant) coroutine असाइन करती है। दूसरी लाइन orders_coro को एक अन्य सुप्त coroutine असाइन करती है। जब एक्जीक्यूशन await user_coro पर पहुँचता है, तो Python अंततः पहले टास्क को शुरू करता है और उसे पूरा होने तक चलाता है। fetch_user खत्म होने के बाद ही इंटरप्रेटर await orders_coro तक पहुँचता है और दूसरा टास्क शुरू करता है। आपका कुल एक्जीक्यूशन समय दोनों I/O ऑपरेशन्स का योग होता है, न कि सबसे लंबा वाला। आपने उन्हें पैरेलल (parallel) में नहीं चलाया। आपने उन्हें अतिरिक्त स्टेप्स के साथ एक के बाद एक चलाया।
यह बग अदृश्य क्यों है
यह उस तरह का परफॉरमेंस रिग्रेशन (performance regression) है जो महीनों तक बना रहता है। कोड वैध Python है। यह टाइप चेकर्स को पास कर देता है। यह सही परिणाम देता है। यह बस आधी गति से, या उससे भी बदतर गति से चलता है। क्योंकि कोई स्टैक ट्रेस (stack trace) और कोई चेतावनी नहीं होती, इसलिए इंजीनियरिंग टीमें अक्सर पहले कहीं और तलाश करती हैं। वे Redis कैश जोड़ते हैं, डेटाबेस टियर अपग्रेड करते हैं, या होस्टिंग रीजन बदलते हैं। असली अपराधी await वास्तव में क्या करता है, इस बारे में उम्मीदों का एक सूक्ष्म अंतर (subtle mismatch) है।
Python को वास्तव में चीज़ों को कॉनकरेंटली चलाने के तीन तरीके
इसे ठीक करने के लिए, आपको Python के इवेंट लूप को काम को तुरंत शेड्यूल करने के लिए कहना होगा। आपको एक रॉ (raw) coroutine से अधिक सक्रिय चीज़ की आवश्यकता है। आपको एक Task की आवश्यकता है।
1. asyncio.create_task
JavaScript पैटर्न का सबसे सीधा अनुवाद अपने coroutine को एक Task में लपेटना (wrap) है। एक Task को जैसे ही आप बनाते हैं, उसे इवेंट लूप पर शेड्यूल कर दिया जाता है। यह चलते हुए JavaScript Promise के सबसे करीब Python समकक्ष है।
user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))
user = await user_task
orders = await_orders_task
अब पहले await से पहले ही fetch_user और fetch_orders दोनों चल रहे हैं। जब आप await user_task पर पहुँचते हैं, तो आप केवल तब तक रुकते हैं जब तक कि वह विशिष्ट Task पूरा नहीं हो जाता, लेकिन दूसरा Task चलता रहता है। यदि fetch_orders पहले समाप्त हो जाता है, तो इसका परिणाम orders_task के अंदर तब तक प्रतीक्षा करता है जब तक कि आप उसे न मांग लें।
हालाँकि, सावधान रहें। यदि आप एक Task बनाते हैं और उसे कभी await नहीं करते हैं, तो Python एक नष्ट किए गए पेंडिंग टास्क (destroyed pending task) के बारे में एरर देगा। आपको अपने परिणाम अभी भी एकत्र करने होंगे।
2. asyncio.gather
यदि आपके पास कई coroutines हैं जिन्हें आगे बढ़ने से पहले समाप्त करने की आवश्यकता है, तो asyncio.gather आपके लिए बॉयलरप्लेट (boilerplate) को संभालता है। यह आंतरिक रूप से प्रत्येक coroutine को एक Task के रूप में शेड्यूल करता है और उन्हें एक साथ await करता है।
user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))
यह संक्षिप्त और पठनीय है। यह तब चमकता है जब ऑपरेशन्स स्वतंत्र होते हैं और आप एक ऐसी सिंगल लाइन चाहते हैं जो कहती हो "इन सभी को चलाओ, फिर मुझे हर परिणाम दो।" यह लौटाए गए लिस्ट या टुपल (tuple) में आर्गुमेंट्स के क्रम को भी सुरक्षित रखता है, भले ही अंतर्निहित टास्क अलग क्रम में पूरे हुए हों।
3. asyncio.TaskGroup
Python 3.11 में TaskGroup पेश किया गया, जो standard library में structured concurrency लेकर आता है। हाथों से tasks बनाने के बजाय, आप एक context manager का उपयोग करते हैं जो यह सुनिश्चित करता है कि हर spawned task ठीक से समाप्त हो जाए। यदि एक task exception raise करता है, तो अन्य अपने आप cancel हो जाते हैं।
async with asyncio.TaskGroup() as tg:
user_task = tg.create_task(fetch_user(id))
orders_task = tg.create_task(fetch_orders(id))
user = user_task.result()
orders = orders_task.result()
यह pattern जटिल workflows के लिए बेहतरीन है। यह किसी Task के orphan होने के जोखिम को समाप्त करता है, और संबंधित operations के lifecycle को एक ही logical umbrella के तहत समूहित करता है। यदि आपका codebase Python 3.11 या उससे नए वर्ज़न पर चलता है, तो fan-out concurrency के लिए यह अक्सर सबसे साफ़ (cleanest) architecture होता है।
मेंटल मॉडल: await का अर्थ है "इसे अभी चलाएं"
मुख्य सबक भाषाई है। JavaScript में, आप await को "इस बीच" (meanwhile) के रूप में पढ़ सकते हैं। आप काम शुरू करते हैं, अन्य काम करते हैं, और केवल तभी रुकते हैं जब आपको value की आवश्यकता होती है। Python में, await का अर्थ है "इस coroutine को इसके अगले suspension point या completion तक ले जाएं।" यदि coroutine को अभी तक schedule नहीं किया गया है, तो await ही उसे schedule करता है। यही कारण है कि आप दो raw coroutines शुरू करके बाद में उन्हें await नहीं कर सकते। आपने इस बीच event loop को करने के लिए कुछ भी नहीं दिया है।
Python coroutines को generator functions की तरह समझें। Generator को कॉल करने से वह iterate नहीं होता है। आपको इसके ऊपर loop चलाना होगा, next() कॉल करना होगा, या इसे किसी consumer को पास करना होगा। Async भी इसी तरह काम करता है। asyncio.create_task वह consumer है जो कहता है "इसे अभी event loop पर डाल दें।" इसके बाद वाला await बस finished signal का इंतज़ार करता है।
एक ठोस आदत जो मदद करती है: जब भी आप await के बिना किसी async function call को variable में assign करते हैं, तो खुद से पूछें कि क्या आपने इसे schedule किया है। यदि right-hand side create_task, gather, या TaskGroup में लिपटा (wrapped) नहीं है, तो यह चल नहीं रहा है। यह केवल काउंटर पर रखी एक रेसिपी की तरह है।
निष्कर्ष
Python का async runtime शक्तिशाली है, लेकिन यह स्पष्ट इरादे (explicit intent) की मांग करता है। भाषा केवल इसलिए background work शुरू नहीं करती क्योंकि आपने किसी function को कॉल किया है। यदि आप JavaScript से आ रहे हैं, तो हर उस जगह की जांच (audit) करें जहाँ आप एक coroutine को variable में स्टोर करते हैं और बाद में उसे await करते हैं। जब तक आपने इसे पहले Task में प्रमोट नहीं किया, तब तक आपने async के लिबास में sequential code लिखा है। काम को Task के साथ शुरू करें, फिर परिणामों का इंतज़ार करें। इसी तरह आप Python async को एक साइलेंट बॉटलनेक (silent bottleneck) से एक वास्तविक concurrency टूल में बदल सकते हैं।
