8.4. Gather

הפרק הקודם הראה כיצד asyncio.create_task() מתזמן קורוטינה ואינו ממתין לה. הפעולה המקבילה – להריץ כמה awaitables במקביל ולהמתין לכולם – היא asyncio.gather().

8.4.1. הצורה הבסיסית

import asyncio

async def fetch(name, delay_ms):
    await asyncio.sleep_ms(delay_ms)
    return name

async def main():
    results = await asyncio.gather(
        fetch("a", 100),
        fetch("b", 200),
        fetch("c", 50),
    )
    print(results)

asyncio.run(main())

פלט:

['a', 'b', 'c']

שני דברים שיש לשים לב אליהם. ראשית, רשימת התוצאות היא בסדר הארגומנטים ל-gather, ולא בסדר שבו הקורוטינות הסתיימו – "c" חזרה ראשונה אך עדיין מהווה את הערך השלישי. שנית, הקריאה ארכה 200 ms בסך הכול, ולא 350 ms: שלוש פעולות ה-sleep רצו במקביל, ו-gather() חוזרת ברגע שהאיטית ביותר מסתיימת.

שתי העובדות נובעות מאותו מקור. gather() עוטף כל ארגומנט שאינו כבר משימה (הקורוטינות שבדוגמה), מתזמן אותם בלולאה, משעה את הקורוטינה הקוראת עד שכולם מסתיימים, ואז מחזיר את תוצאותיהם בסדר המקורי.

8.4.2. מתי לפנות אליה

בכל מקום שבו ליישום יש N פעולות awaitable והוא רוצה את התוצאות של כולן לפני שימשיך. דוגמאות טיפוסיות:

  • הנפקת מספר בקשות רשת במקביל והמתנה לכל התגובות.

  • קריאה ממספר חיישנים במקביל לפני עיבוד התוצאה המשולבת.

  • איחוד מספר משימות עזר קצרות-חיים בסוף שלב של יישום.

זה אינו הכלי הנכון למשימות רקע ארוכות-ריצה שהיישום מתחיל ומשאיר רצות למשך כל חיי התוכנית – אלה עדיין עבודה של create_task(). gather() מיועד לדפוס ה-fan-out / fan-in: לפצל עבודה, לבצע אותה במקביל, לאחד מחדש.

8.4.3. חריגות בקבוצה

אם כל אחד מה-awaitables שנאספו מעלה חריגה, ההתנהגות שכברירת מחדל היא להעלות מחדש את החריגה החוצה מקריאת ה-gather. האחים שטרם הסתיימו מבוטלים ברקע.

זה בדרך כלל מה שיישום רוצה – אחת מ-N עבודות מקביליות נכשלה, ולכן הפעולה המשולבת נכשלה, ולכן הפסק לבזבז זמן על השאר. לעיתים היישום רוצה את ההפך: לתת לכל awaitable להסתיים (או להיכשל) באופן עצמאי, ולבחון את התוצאות לאחר מכן. העבר return_exceptions=True לשם כך:

results = await asyncio.gather(
    fetch_or_fail("ok"),
    fetch_or_fail("bad"),
    return_exceptions=True,
)
# results == ["ok-value", OSError(...)]

כל ערך ברשימה המוחזרת הוא כעת או ערך החזרה רגיל או החריגה שה-awaitable המתאים העלה. הקורא בודק isinstance(r, Exception) כדי להבחין ביניהם.

8.4.4. ביטול

ביטול ה-gather עצמו – על ידי ביטול המשימה שהמתינה לו – מבטל כל awaitable שעדיין רץ בתוכו. עמוד פסקי זמן וביטול מכסה בפירוט כיצד ביטול מתפשט דרך שרשרת הקריאות.