3 पॉइंट द्वारा GN⁺ 2024-05-09 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Python का asyncio I/O-bound प्रोग्राम्स को संभालने का एक टूल है, और यह लेख इसे generators के ऊपर एक सरल implementation के रूप में फिर से बनाकर इसकी कार्यप्रणाली दिखाता है
  • मूल विचार यह है कि कई tasks वाला event loop tasks को चलाता है, yield पर नियंत्रण वापस लेता है, और फिर अगले task पर बढ़ता है
  • sleep को एक ऐसे sub-generator के रूप में बनाया जा सकता है जो समय बीतने तक yield करता रहे, और yield from sub-generator के खत्म होने के बाद मूल task को फिर से जारी रखता है
  • async/await में बदलने पर Task.__await__() पूरा होने तक नियंत्रण event loop को सौंपता है, और create_taskrun का रूप असली asyncio API जैसा बन जाता है
  • custom jacobio calls को 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 नहीं है, तो StopIteration exception आता है
  • yield from generator को किसी 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 True loop है, इसलिए execution जारी रहता है
    • output Task 1, Task 2, Task 1, Task 2 की तरह दोहराता है

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 का while loop समाप्त हो जाता है
    • अब और yield नहीं होने पर StopIteration आता है, और yield from task function की अगली line पर execution जारी रखता है
  • उदाहरण में task1 हर 1 सेकंड पर print करता है और task2 हर 5 सेकंड पर print करता है
    • output में Task 1, Task 2 के बाद Task 1 कई बार आता है और फिर Task 2 फिर से आता है

yield से await तक

  • await का उपयोग करने के लिए target object में __await__ method होना चाहिए या वह coroutine होना चाहिए
  • asyncio में आम तौर पर asyncio.create_task जैसे function के ज़रिए Task object का उपयोग किया जाता है
    • Task object, asyncio के Future object को inherit करता है
    • Future object में __await__ method होता है
  • async keyword वाले function को call करने पर coroutine object बनता है
    • coroutine भी generator function की तरह execution रोककर बाद में फिर से शुरू कर सकता है
  • await को अतिरिक्त validation rules वाले yield from जैसा समझा जा सकता है
    • await object में object instance के __await__ के भीतर yield होता है, या वह किसी दूसरे coroutine का इंतज़ार करता है
  • asyncio के Future source code में भी देखा जा सकता है कि Future या Task पूरा न होने पर __await__ मूल रूप से yield call करता है

सरल Task, create_task, run implementation

  • custom implementation list की जगह event loop के लिए Queue का उपयोग करती है
    • इसका उद्देश्य loop में task जोड़ने और हटाने को constant time में संभालना है
  • Task class generator object और completion status store करती है
    • generator object को self.iter में रखा जाता है
    • self.finished की शुरुआत False से होती है
    • अगर generator StopIteration उठाता है, तो उसे complete माना जाता है
    • done() completion status return करता है
  • Task.__await__() तब तक लगातार yield self call करता है जब तक 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 = True set किया जाता है
    • 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
    • _sleep
    • async sleep
    • Task
    • create_task
    • run
  • उपयोग के उदाहरण में पुराने yield from को await से बदला गया है, और await इस्तेमाल करने वाले functions में async जोड़ा गया है
    • task1 दो बार print करता है और हर बार jacobio.sleep(1) का इंतज़ार करता है
    • task2 तीन बार print करता है और हर बार jacobio.sleep(0) का इंतज़ार करता है
    • main दो tasks बनाता है, दोनों को await करता है, और फिर done print करता है
  • उदाहरण 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 टिप्पणियां

 
GN⁺ 2024-05-09
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 हो रही है या नहीं

    • शायद यह बुरा idea हो, लेकिन सोचता हूँ कि मौजूदा तरीके—जहाँ हर call default रूप से synchronous होती है—के उलट किया जाए तो कैसा रहेगा
      arithmetic operations या data structure access जैसे basic operations को तेज़ synchronous calls के तौर पर allowlist में डाल दिया जाए, और अगर event loop संदिग्ध रूप से बहुत तेज़ घूमे, तो शायद उन दूसरे कामों को भी detect किया जा सके जिन्हें synchronous होना चाहिए
    • Temporal सच में कमाल है
      asyncio event loop को उन्होंने कैसे implement किया है, यह समझना सच में दिमाग खोल देने वाला पल था
    • सोच रहा हूँ कि threads क्यों नहीं इस्तेमाल करते
      मैं अभी भी समझने की कोशिश कर रहा हूँ कि क्या Python सच में concurrency के लिए उपयुक्त भाषा है। Asyncio हमेशा जैसे किसी तरह टिके रहने जैसा लगा है, और मेरे हिसाब से C# की async implementation ज़्यादा साफ़-सुथरी है
    • workflow example में Purchaser.purchase को do_purchase नहीं होना चाहिए?
    • उसे statically पकड़ना आसान नहीं होगा
      एक साधारण 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 के साथ इस्तेमाल किया जा सके

    • asyncio में भी built-in implementation के अलावा custom event loop implementations इस्तेमाल की जा सकती हैं
      मशहूर implementations में से एक uvloop(https://github.com/MagicStack/uvloop) है, जो मूल रूप से loop implement करने के लिए libuv का इस्तेमाल करता है, और आपके बताए select जैसे काम libuv संभालता है
    • यह SimPy [1] / Simpy.io [2] से काफ़ी मिलता-जुलता लगता है, लेकिन SimPy asyncio से कई साल पहले आया था
      [1] https://simpy.readthedocs.io/en/latest/
      [2] https://gitlab.com/team-simpy/simpy.io
    • एक नज़रिए से इस approach में कोई खास समस्या नहीं है
      event loop को ज़रूरी नहीं कि loop करते रहना ही पड़े; वह main को run करके शुरू हो और उसके खत्म होते ही साथ में खत्म हो जाए, यह भी ठीक है। एक server की कल्पना करें जिसमें socket का इंतज़ार करने वाला while true loop शुरू होता है, और किसी 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 तो बना सकते थे

    • वह function ही है, बस उसका return type किसी दूसरे function में बदल गया है
      यह 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 पसंद करता हूँ
    • असली समस्या क्या है? या यह बस language purity की बात है?
    • इसे tragedy मानने की बात नहीं है
      ऐसा लगता है कि developer experience की semantics को language purity जैसी चीज़ों से ऊपर रखा गया है। async function asynchronous function है, इस वजह से मुझे कभी confusion नहीं हुआ
    • Python की बड़ी समस्या तो यह है कि function के लिए शुरू से ही def लिखना पड़ता है, इसलिए JS arrow functions या C++ lambdas की तरह inline functions नहीं बना सकते
      यह language purity का मुद्दा नहीं, बस असुविधाजनक है
    • क्या decorator की बात कर रहे हैं? Python के context में आप क्या कह रहे हैं, यह समझना काफ़ी confusing है