2025, Sep 24 13:33

asyncio में tasks कब चलती हैं और क्यों: उदाहरण सहित

Python asyncio में tasks कब शुरू होती हैं? event loop शेड्यूलिंग, create_task और await का व्यवहार, Python 3.12 की eager tasks, pitfalls को उदाहरणों से समझें.

यह समझना कि asyncio की tasks वास्तव में कब चलना शुरू करती हैं, ऐसा बिंदु है जो समानांतर कोड को अप्रत्याशित बना सकता है। आइए एक न्यूनतम उदाहरण से गुजरें, देखें कि इवेंट लूप क्या कर रहा है, और स्पष्ट करें कि पहले आप “Do something with 1...” क्यों देखते हैं, जबकि आपने पहले task 2 का await किया है।

समस्या का सेटअप

इस प्रोग्राम पर गौर करें। यह दो tasks बनाता है, फिर पहले task का इंतज़ार करने से पहले दूसरे का await करता है। दिखाई देने वाला व्यवहार ही उलझन का कारण है।

import asyncio
async def get_payload(delay):
    print(f"Do something with {delay}...")
    await asyncio.sleep(delay)
    print(f"Done with {delay}")
    return f"Result of {delay}"
async def orchestrate():
    job_a = asyncio.create_task(get_payload(1))
    job_b = asyncio.create_task(get_payload(2))
    out_b = await job_b
    print("Task 2 fully completed")
    out_a = await job_a
    print("Task 1 fully completed")
    return [out_a, out_b]
outputs = asyncio.run(orchestrate())
print(outputs)

आउटपुट:

Do something with 1...
Do something with 2...
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']

पहली नज़र में आपको लग सकता है कि “Do something with 2...” संदेश 1 से जुड़े किसी भी संदेश से पहले आना चाहिए, क्योंकि await task 2 पर किया गया है। लेकिन इवेंट लूप का शेड्यूलिंग बताता है कि task 1 को पहले बोलने का मौका क्यों मिलता है।

असल में क्या होता है

जब आप asyncio.create_task(get_payload(1)) और asyncio.create_task(get_payload(2)) कॉल करते हैं, तो दोनों tasks बन जाती हैं और शेड्यूल होने के योग्य होती हैं। वे वर्तमान में चल रही coroutine को बाधित नहीं करतीं। orchestrate coroutine तब तक चलती रहती है जब तक वह किसी await तक नहीं पहुंच जाती या पूरी नहीं हो जाती। उसी के बाद इवेंट लूप अन्य tasks चला सकता है।

मुख्य क्षण है out_b = await job_b। यह await नियंत्रण वापस इवेंट लूप को दे देता है और अन्य तैयार tasks को चलने देता है। अगली शेड्यूल होने वाली task वही होती है जो सबसे पहले बनाई गई थी। इसलिए job_a चलना शुरू करती है और अपने पहले await तक पहुँचने तक चलती है—वह “Do something with 1...” प्रिंट करती है और फिर asyncio.sleep(1) का इंतज़ार करती है। अब job_a निलंबित होने पर, लूप job_b को शेड्यूल करता है, जो “Do something with 2...” प्रिंट करती है और asyncio.sleep(2) का इंतज़ार करती है। इसके बाद, जैसे-जैसे प्रत्येक task का sleep पूरा होता है, वे फिर से शुरू होती हैं—इसीलिए “Done with 1” “Done with 2” से पहले दिखता है।

एक आसान दृश्यता तरकीब

शेड्यूलिंग का बिंदु साफ़ दिखाने के लिए, दोनों tasks बनाने के तुरंत बाद और पहले await से पहले यह पंक्ति जोड़ें:

import asyncio
async def get_payload(delay):
    print(f"Do something with {delay}...")
    await asyncio.sleep(delay)
    print(f"Done with {delay}")
    return f"Result of {delay}"
async def orchestrate():
    job_a = asyncio.create_task(get_payload(1))
    job_b = asyncio.create_task(get_payload(2))
    print('tasks created')
    out_b = await job_b
    print("Task 2 fully completed")
    out_a = await job_a
    print("Task 1 fully completed")
    return [out_a, out_b]
outputs = asyncio.run(orchestrate())
print(outputs)

सबसे पहले जो पंक्ति दिखेगी, वह होगी:

tasks created

ऐसा इसलिए होता है क्योंकि tasks बनाना वर्तमान में चल रही coroutine को बाधित नहीं करता। केवल जब orchestrate किसी await पर पहुँचती है, तभी नियंत्रण इवेंट लूप को लौटता है, जो फिर अन्य tasks को एक-एक करके चलाना शुरू करता है। चूँकि job_a सबसे पहले बनाई गई थी, वह पहले शेड्यूल होती है, और आपको पूरा क्रम इस तरह मिलेगा:

tasks created
Do something with 1...
Do something with 2...
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']

“लेकिन मैंने await नहीं किया, फिर भी वे प्रिंट कैसे हुए”

एक और आम बात यह दिखती है कि केवल tasks बनाना भी coroutines को चलाता हुआ लगता है, भले ही आप उन्हें कभी await न करें:

import asyncio
async def get_payload(delay):
    print(f"Do something with {delay}...")
    await asyncio.sleep(delay)
    print(f"Done with {delay}")
    return f"Result of {delay}"
async def orchestrate():
    job_a = asyncio.create_task(get_payload(1))
    job_b = asyncio.create_task(get_payload(2))
outputs = asyncio.run(orchestrate())
print(outputs)

आउटपुट:

Do something with 1...
Do something with 2...
None

यह उसी शेड्यूलिंग नियम के अनुरूप है। orchestrate के चलते समय नई बनाई गई tasks नहीं चल सकतीं। जैसे ही orchestrate पूरा होता है, नियंत्रण इवेंट लूप को लौटता है और वह तैयार tasks चला सकता है। प्रत्येक task शुरू होते ही उसकी शुरुआती प्रिंट तुरंत हो जाती है। sleep के बाद वाली प्रिंट इसलिए नहीं दिखती क्योंकि आपने tasks के पूरा होने का इंतज़ार नहीं किया। आख़िरी प्रिंट None है, क्योंकि orchestrate कुछ लौटाता नहीं है।

Python 3.12+ में eager tasks

Python 3.12 और उसके बाद के संस्करणों में tasks को eager तरीके से बनाया जा सकता है। इस मोड में, tasks बनते ही तुरंत चलना शुरू कर देती हैं:

import asyncio
async def get_payload(delay):
    print(f"Do something with {delay}...")
    await asyncio.sleep(delay)
    print(f"Done with {delay}")
    return f"Result of {delay}"
async def orchestrate():
    running = asyncio.get_running_loop()
    running.set_task_factory(asyncio.eager_task_factory)
    job_a = asyncio.create_task(get_payload(1))
    job_b = asyncio.create_task(get_payload(2))
    print('tasks created')
    out_b = await job_b
    print("Task 2 fully completed")
    out_a = await job_a
    print("Task 1 fully completed")
    return [out_a, out_b]
outputs = asyncio.run(orchestrate())
print(outputs)

इस स्थिति में आप यह देखेंगे:

Do something with 1...
Do something with 2...
tasks created
Done with 1
Done with 2
Task 2 fully completed
Task 1 fully completed
['Result of 1', 'Result of 2']

फर्क बस इतना है कि task factory यह बदल देती है कि tasks कब चलना शुरू करती हैं।

यह क्यों मायने रखता है

इवेंट लूप में नियंत्रण सौंपने के बिंदु asyncio की concurrency को परिभाषित करते हैं। जैसे ही आप yield करते हैं, दूसरी tasks चल सकती हैं, और उनका क्रम इस पर निर्भर करता है कि वे कब बनाई या शेड्यूल की गई थीं। इसे समझना इस गलत धारणा से बचाता है कि कौन-सी coroutine पहले चलेगी, और उन सूक्ष्म बग्स से भी, जहाँ tasks तो बना दी जाती हैं पर उनका await नहीं किया जाता, नतीजतन काम अधूरा रह जाता है।

मुख्य निष्कर्ष

जब आपको concurrency चाहिए, तो asyncio.create_task से tasks बनाइए, पर याद रखें: वे तभी चलेंगी जब आपका कोड yield करेगा या पूरा होगा—जब तक कि आप Python 3.12+ में eager व्यवहार न चुनें। अगर परिणाम चाहिए, तो बनाई गई tasks का await कीजिए। केवल tasks बनाकर लौट आएँगे, तो उन tasks के पहले await से पहले होने वाले तत्काल side effects तो दिखेंगे, पर न पूर्णता मिलेगी और न await के बाद वाला आउटपुट।

यह लेख StackOverflow के प्रश्न (लेखक: M a m a D) और Booboo के उत्तर पर आधारित है।