Python asyncio कैसे काम करता है: शुरुआत से फिर से बनाकर समझना
(jacobpadilla.com)- Python का asyncio I/O-bound प्रोग्राम्स को संभालने का एक टूल है, और यह लेख इसे generators के ऊपर एक सरल implementation के रूप में फिर से बनाकर इसकी कार्यप्रणाली दिखाता है
- मूल विचार यह है कि कई tasks वाला event loop tasks को चलाता है,
yieldपर नियंत्रण वापस लेता है, और फिर अगले task पर बढ़ता है sleepको एक ऐसे sub-generator के रूप में बनाया जा सकता है जो समय बीतने तकyieldकरता रहे, औरyield fromsub-generator के खत्म होने के बाद मूल task को फिर से जारी रखता हैasync/awaitमें बदलने परTask.__await__()पूरा होने तक नियंत्रण event loop को सौंपता है, औरcreate_taskवrunका रूप असली asyncio API जैसा बन जाता है- custom
jacobiocalls कोasyncioमें बदलने पर कोड असली package इस्तेमाल करने लगता है, लेकिन वास्तविक asyncio उदाहरण की तुलना में कहीं अधिक जटिल है और उसका internal flow भी कुछ हद तक अलग है
Generators के ज़रिए asyncio की बुनियादी संरचना
- asyncio का उपयोग Python में I/O-bound programs को संभालने के लिए किया जाता है, और यह लेख इसे generator-आधारित सरल रूप में दोबारा बनाकर समझाता है
- Python 3+ के
rangeकी तरह, generators पूरी sequence को memory में store नहीं करते, बल्कि ज़रूरत पड़ने पर एक-एक value बनाते हैं- अगर
range(100_000_000)को list की तरह बनाया जाए, तो 10 करोड़ elements store करने पड़ेंगे, जो memory के लिहाज़ से अक्षम है - generator values को ज़रूरत पड़ने पर बनाता है, इसलिए पूरी sequence को संभालकर नहीं रखता
- अगर
- generator function सामान्य function की तरह define होती है, लेकिन उसमें
yieldका उपयोग होता है- function call करने पर body तुरंत execute नहीं होती, बल्कि एक generator object return होता है
next(iterator)call करने पर execution अगलेyieldतक चलता है- अगर आगे कोई
yieldनहीं है, तोStopIterationexception आता है
yield fromgenerator को किसी sub-generator या iterable object को call करने देता है, जिससे generator chain बनाई जा सकती है- यहाँ महत्वपूर्ण गुण यह है कि function execution को रोका जा सकता है और state बनाए रखते हुए बाद में फिर से शुरू किया जा सकता है
Event loop को list से सरल बनाना
- Event loop asyncio का core है, जो मौजूदा tasks को चलाता और manage करता है
- असली asyncio event loop C में लिखा गया है, लेकिन इस सरल मॉडल में इसे current tasks को रखने वाली list की तरह देखा जा सकता है
- शुरुआती उदाहरण में task को generator object माना गया है
- event loop manager task list को iterate करता है
- हर task पर
next(task)call करके उसे चलाया जाता है - task, I/O-bound काम की तरह, इंतज़ार वाले बिंदु पर
yieldकरके रुक जाता है और event loop को नियंत्रण लौटा देता है
task1()औरtask2()के उस उदाहरण में जहाँ दोनों print करने के बादyieldकरते हैं, output बारी-बारी से दिखाई देता है- दोनों functions में
while Trueloop है, इसलिए execution जारी रहता है - output
Task 1,Task 2,Task 1,Task 2की तरह दोहराता है
- दोनों functions में
yield from से sleep बनाना
sleep(seconds)को एक ऐसे generator के रूप में implement किया गया है जो start time record करता है और तय समय बीतने तक लगातारyieldकरता रहता है- task function execution को
yield from sleep(1)याyield from sleep(5)की तरह sleep sub-generator को सौंप देता है- जब तक
sleepलगातारyieldकरता रहता है, task रुकी हुई स्थिति में रहता है - पर्याप्त समय बीतने पर
sleepकाwhileloop समाप्त हो जाता है - अब और
yieldनहीं होने परStopIterationआता है, औरyield fromtask function की अगली line पर execution जारी रखता है
- जब तक
- उदाहरण में
task1हर 1 सेकंड पर print करता है औरtask2हर 5 सेकंड पर print करता है- output में
Task 1,Task 2के बादTask 1कई बार आता है और फिरTask 2फिर से आता है
- output में
yield से await तक
awaitका उपयोग करने के लिए target object में__await__method होना चाहिए या वह coroutine होना चाहिए- asyncio में आम तौर पर
asyncio.create_taskजैसे function के ज़रिएTaskobject का उपयोग किया जाता हैTaskobject, asyncio केFutureobject को inherit करता हैFutureobject में__await__method होता है
asynckeyword वाले function को call करने पर coroutine object बनता है- coroutine भी generator function की तरह execution रोककर बाद में फिर से शुरू कर सकता है
awaitको अतिरिक्त validation rules वालेyield fromजैसा समझा जा सकता हैawait objectमें object instance के__await__के भीतरyieldहोता है, या वह किसी दूसरे coroutine का इंतज़ार करता है
- asyncio के
Futuresource code में भी देखा जा सकता है किFutureयाTaskपूरा न होने पर__await__मूल रूप सेyieldcall करता है
सरल Task, create_task, run implementation
- custom implementation list की जगह event loop के लिए
Queueका उपयोग करती है- इसका उद्देश्य loop में task जोड़ने और हटाने को constant time में संभालना है
Taskclass generator object और completion status store करती है- generator object को
self.iterमें रखा जाता है self.finishedकी शुरुआतFalseसे होती है- अगर generator
StopIterationउठाता है, तो उसे complete माना जाता है done()completion status return करता है
- generator object को
Task.__await__()तब तक लगातारyield selfcall करता है जब तक task खत्म नहीं हो जाता- यही behavior event loop को नियंत्रण वापस देता है
create_task(generator)generator कोTaskमें wrap करके event loop queue में डालता है और return करता है- इसका काम task को event loop में schedule करना है
run(main)असलीasyncio.run()जैसा दिखने वाला function है जो event loop शुरू करता है- शुरुआत में मिले
mainकोTaskमें wrap करके queue में डाला जाता है - जब तक queue खाली नहीं होती, अगला task निकाला जाता है
task.iter.send(None)से task को आगे बढ़ाया जाता हैStopIterationआने परtask.finished = Trueset किया जाता है- exception न हो तो task को फिर से event loop queue में डाल दिया जाता है
- शुरुआत में मिले
next(task.iter)की जगहtask.iter.send(None)का उपयोगasync/awaitके साथ काम करते समय जुड़ी विशेषताओं के कारण किया जाता है, हालांकि यहाँ दोनों की भूमिका एक जैसी है
async-compatible sleep और jacobio उदाहरण
- पुराना
sleepएक generator function था, लेकिनawaitको सीधे generator function के साथ नहीं जोड़ा जा सकताawaitका target या तो__await__वाला object होना चाहिए, या coroutine function होना चाहिए
- असली wait logic को
_sleep(seconds)generator में ले जाया गया है_sleepतय समय बीतने तकyieldकरता है
async def sleep(seconds)_sleep(seconds)से task बनाता है और फिर उस task कोawaitकरता हैawait task,Task.__await__()को call करता है- अगर task पूरा नहीं हुआ है, तो
yieldके ज़रिए event loop को नियंत्रण सौंप दिया जाता है
- पूरी custom file
jacobio.pyमें ये तत्व शामिल हैं- event loop queue
_sleepasync sleepTaskcreate_taskrun
- उपयोग के उदाहरण में पुराने
yield fromकोawaitसे बदला गया है, औरawaitइस्तेमाल करने वाले functions मेंasyncजोड़ा गया हैtask1दो बार print करता है और हर बारjacobio.sleep(1)का इंतज़ार करता हैtask2तीन बार print करता है और हर बारjacobio.sleep(0)का इंतज़ार करता हैmainदो tasks बनाता है, दोनों कोawaitकरता है, और फिरdoneprint करता है
- उदाहरण output का क्रम
Task 1,Task 2,Task 2,Task 2,Task 1,doneहै
असली asyncio में बदलना
- custom
jacobioउदाहरण मेंjacobioको पूरी तरहasyncioसे बदल देने पर कोड असली asyncio package का उपयोग करने लगता है - संबंधित functions इस प्रकार हैं
jacobio.sleep()→asyncio.sleep()jacobio.create_task()→asyncio.create_task()jacobio.run()→asyncio.run()
- असली asyncio अंदरूनी तौर पर इससे कहीं अधिक काम करता है
- यह event loop manager यथासंभव सरल implementation है, इसलिए यह asyncio का मूल विचार तो दिखाता है, लेकिन असली package के आकार और जटिलता के कारण यह वास्तविक source code flow से थोड़ा अलग है
- असली asyncio में दोनों tasks अलग-अलग बनाकर दोनों को
awaitकरने के बजायasyncio.gather()जैसे function से कई tasks को संभाला जा सकता है - संबंधित लेख के रूप में handling asyncio tasks like a pro जोड़ा गया है
1 टिप्पणियां
Hacker News टिप्पणियाँ
Asyncio में event loop को अपनी बनाई हुई implementation से बदला जा सकता है
Temporal Python में workflows को custom durable asyncio event loop के रूप में व्यक्त किया जाता है, इसलिए
asyncio.sleepजैसी चीज़ भी durable timer बन जाती है। यानी code किसी दूसरी machine पर resume हो सकता है, इसलिए कई हफ्तों तक sleep करना भी संभव हैimplementation का तरीका इस लेख में समझाया गया है: https://temporal.io/blog/durable-distributed-asyncio-event-l...
asyncio की सबसे बड़ी समस्या यह है कि Python में synchronous call से asyncio thread को block करके पूरे system को आसानी से रोक दिया जा सकता है। Python को ऐसे static analysis tools की सख्त ज़रूरत है जो call graph बनाकर यह detect करें कि
async defके अंदर thread को block करने वाली call direct या indirect रूप से call हो रही है या नहींarithmetic operations या data structure access जैसे basic operations को तेज़ synchronous calls के तौर पर allowlist में डाल दिया जाए, और अगर event loop संदिग्ध रूप से बहुत तेज़ घूमे, तो शायद उन दूसरे कामों को भी detect किया जा सके जिन्हें synchronous होना चाहिए
asyncio event loop को उन्होंने कैसे implement किया है, यह समझना सच में दिमाग खोल देने वाला पल था
मैं अभी भी समझने की कोशिश कर रहा हूँ कि क्या Python सच में concurrency के लिए उपयुक्त भाषा है। Asyncio हमेशा जैसे किसी तरह टिके रहने जैसा लगा है, और मेरे हिसाब से C# की async implementation ज़्यादा साफ़-सुथरी है
Purchaser.purchaseकोdo_purchaseनहीं होना चाहिए?एक साधारण
read()भी descriptor क्या है और कैसे set किया गया है, इस पर निर्भर करते हुए block कर सकता है या नहीं भी कर सकता। इसे static analysis से कैसे detect करेंगे?यह implementation sleep करते समय busy waiting करती हुई लगती है। यानी अभी कोई runnable task न होने पर भी event loop चलता रहता है
मुझे याद है पहले एक दूसरी toy implementation देखी थी, जिसमें tasks के अगले runnable time को sorted order में track किया जाता था, और अगर अभी कोई runnable task न हो तो event loop खुद sleep कर जाता था। मेरी समझ में असली asyncio भी इसी तरह काम करता है
बाद में इसे इस तरह extend किया जा सकता है कि अगली runnable condition सिर्फ wall-clock time न होकर socket जैसी चीज़ों पर dependency भी रख सके, ताकि
selectको timeout के साथ इस्तेमाल किया जा सकेमशहूर implementations में से एक uvloop(https://github.com/MagicStack/uvloop) है, जो मूल रूप से loop implement करने के लिए libuv का इस्तेमाल करता है, और आपके बताए
selectजैसे काम libuv संभालता है[1] https://simpy.readthedocs.io/en/latest/
[2] https://gitlab.com/team-simpy/simpy.io
event loop को ज़रूरी नहीं कि loop करते रहना ही पड़े; वह
mainको run करके शुरू हो और उसके खत्म होते ही साथ में खत्म हो जाए, यह भी ठीक है। एक server की कल्पना करें जिसमें socket का इंतज़ार करने वालाwhile trueloop शुरू होता है, और किसी termination condition या interrupt पर खत्म होकर program भी बंद हो जाता हैevent loop के नज़रिए से busy waiting नहीं है और sleep या socket को छूने की भी ज़रूरत नहीं है। यह completion तक run करने और हमेशा run करने के बीच का अंतर है
अगर toy loop बना रहे हों, तो मेरे हिसाब से हमेशा चलने वाले case को संभालना बेहतर है कि न ही करें
David Beazley का asyncio talk शानदार है
उसी के आधार पर मैंने discrete-event simulation tool बनाया था। asyncio को खुद implement कर पाना और system clock को simulation time से बदल पाना काफ़ी शानदार है
सच में बेहतरीन लेख है, और पहली बार पढ़ने वाले को उबाऊ लग सकने वाले हिस्सों को अच्छी तरह छोड़कर दिया गया high-level explanation है
अंत में अगर यह बताने वाली कोई और सामग्री जोड़ दी जाए कि अंदर असल में कैसे काम होता है, तो और अच्छा होगा
अगर लेख ने
yieldका बिल्कुल इस्तेमाल किए बिना वही बात दिखाई होती, तो शायद और अच्छा होता। सच कहूँ तो असली जादू जैसा लगने वाला हिस्सा वहीं हैPython coroutines पर कहीं ज़्यादा गहरी explanation यहाँ है: https://aosabook.org/en/500L/a-web-crawler-with-asyncio-coro...
poll()का कोई ज़िक्र ही नहीं? अगर ऐसा है, तो यह asyncio के काम करने के तरीके से बिल्कुल अलग हैPython ने
defको intercept करके उसे असल में function नहीं, बल्कि object बनाने के लिए इस्तेमाल करने दिया—यह पूरी तरह अजीब हैकम से कम कोई अलग keyword तो बना सकते थे
यह generator या coroutine return करने वाला रूप है, और type signature यहाँ देखा जा सकता है [1]। भाषा-स्तर के syntax sugar के बिना भी decorator जैसी चीज़ों से pure Python में कुछ ऐसा ही किया जा सकता है
[1] https://docs.python.org/3/library/typing.html#typing.Generat...
हाँ, type signature का ज़िक्र academic रुचि से किया है; असल type annotations में मैं ज़्यादा सरल
IterableऔरAwaitableपसंद करता हूँऐसा लगता है कि developer experience की semantics को language purity जैसी चीज़ों से ऊपर रखा गया है।
asyncfunction asynchronous function है, इस वजह से मुझे कभी confusion नहीं हुआdefलिखना पड़ता है, इसलिए JS arrow functions या C++ lambdas की तरह inline functions नहीं बना सकतेयह language purity का मुद्दा नहीं, बस असुविधाजनक है