Kodokon kodokon.com

asyncio के साथ एसिंक्रोनस कोड: कोरूटीन, await, gather

asyncio की सहकारी कंकरेंसी समझें: आलसी कोरूटीन, निलंबन बिंदु, gather, TaskGroup और ब्लॉकिंग कॉल के जाल।

10 मिनट · 3 प्रश्न

इस पाठ को Kodokon में खोलें

asyncio एक ही थ्रेड पर सहकारी कंकरेंसी लागू करता है: एक इवेंट लूप उन कोरूटीन का संचालन करता है जो हर await पर स्वेच्छा से नियंत्रण छोड़ देते हैं। एक बुनियादी बिंदु, जिसे अक्सर गलत समझा जाता है: किसी async def फ़ंक्शन को कॉल करने से कुछ नहीं चलता। यह एक निष्क्रिय कोरूटीन ऑब्जेक्ट बनाता है जिसका शरीर तभी आगे बढ़ता है जब उसे await से प्रतीक्षित किया जाए या लूप पर टास्क के रूप में शेड्यूल किया जाए।

PYTHON
import asyncio

async def greet() -> str:
    return "hello"

coro = greet()
print(type(coro).__name__)

async def main() -> None:
    print(await coro)

asyncio.run(main())
greet() कॉल करने से एक कोरूटीन ऑब्जेक्ट बनता है; await से पहले कुछ नहीं चलता।

await एक निलंबन बिंदु चिह्नित करता है: कोरूटीन वहीं जम जाता है, लूप नियंत्रण वापस ले लेता है और किसी दूसरे तैयार टास्क को आगे बढ़ाता है। दो await के बीच, आपका कोड बिना किसी व्यवधान के चलता है - अधिकांश संरचनाओं के लिए किसी लॉक की ज़रूरत नहीं। कई कोरूटीन को समानांतर (I/O के अर्थ में) चलाने के लिए, उन्हें asyncio.gather में लपेटें: एक-एक सेकंड की तीन प्रतीक्षाएँ कुल एक सेकंड में पूरी होती हैं, तीन में नहीं।

PYTHON
import asyncio
import time

async def fetch(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name} done"

async def main() -> None:
    start = time.perf_counter()
    results = await asyncio.gather(
        fetch("a", 1.0),
        fetch("b", 1.0),
        fetch("c", 1.0),
    )
    elapsed = time.perf_counter() - start
    print(results)
    print(f"total: {elapsed:.1f} s")

asyncio.run(main())
तीन समवर्ती प्रतीक्षाएँ: कुल मिलाकर लगभग 1 सेकंड।

डिफ़ॉल्ट रूप से, gather उठाए गए पहले अपवाद को आगे बढ़ाता है, लेकिन बाकी टास्क को पृष्ठभूमि में चलता छोड़ देता है - जो शायद ही कभी आप चाहते हैं। return_exceptions=True के साथ, अपवाद परिणाम के रूप में लौटाए जाते हैं, जिन्हें बाद में छाँटना होता है। Python 3.11 से, asyncio.TaskGroup को प्राथमिकता दें: पहली त्रुटि पर, सभी सहोदर टास्क साफ़-सुथरे ढंग से रद्द कर दिए जाते हैं, और एक से अधिक अपवाद एक ExceptionGroup में समूहित होकर ऊपर आते हैं, जिन्हें except* से पकड़ा जाता है।

PYTHON
import asyncio
import time

def blocking_io() -> str:
    time.sleep(1)
    return "report written"

async def main() -> None:
    results = await asyncio.gather(
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1, result="tick"),
    )
    print(results)

asyncio.run(main())
to_thread ब्लॉकिंग कोड को अलग कर देता है: लूप उत्तरदायी बना रहता है।

ज्ञान जांच

सुनिश्चित करें कि आपको इस पाठ के मुख्य बिंदु याद हैं।

  1. अकेले लिखी गई कॉल fetch('a', 1.0), बिना किसी await या create_task के, क्या उत्पन्न करती है?
    • एक निष्क्रिय कोरूटीन ऑब्जेक्ट: फ़ंक्शन के शरीर की एक भी लाइन नहीं चलती
    • कोरूटीन का तत्काल, सिंक्रोनस निष्पादन
    • इवेंट लूप पर अपने आप शेड्यूल किया गया एक टास्क
    • एक SyntaxError, क्योंकि await अनिवार्य है
  2. asyncio.gather के ज़रिए एक साथ चलाए गए तीन asyncio.sleep(1): कुल कितना समय?
    • लगभग 3 सेकंड, क्योंकि कोड एक ही थ्रेड चलाता है
    • कोरों की संख्या जाने बिना अनुमान लगाना असंभव
    • लगभग 1 सेकंड: प्रतीक्षाएँ आपस में ओवरलैप होती हैं, लूप एक टास्क से दूसरे पर स्विच करता है
  3. किसी कोरूटीन के भीतर time.sleep(2) क्यों वर्जित है?
    • यह async संदर्भ में एक अपवाद उठाता है
    • यह इवेंट लूप के थ्रेड को ब्लॉक कर देता है: 2 सेकंड तक कोई दूसरा टास्क आगे नहीं बढ़ता
    • यह बस asyncio.sleep से कम सटीक है
    • यह दोगुनी मेमोरी इस्तेमाल करता है