8.15. מלכודות

אותם דפוסים שהופכים את asyncio לנעים – ללא קדימה (preemption), awaits מפורשים – מעניקים לו מערך צורות משלו שעלולות לנשוך. עמוד זה הוא הקטלוג של אלה שצצות לעיתים קרובות מספיק כדי שכדאי להכיר אותן.

8.15.1. שכחת await

קריאה לפונקציית async def מחזירה אובייקט קורוטינה. היא אינה מריצה את גוף הפונקציה. כדי להריץ אותה בפועל, יש להמתין (awaited) לקורוטינה או לעטוף אותה במשימה:

async def main():
    send_request()              # bug: returns the coroutine, does nothing
    await send_request()        # right: run it to completion
    asyncio.create_task(send_request())  # right: run it concurrently

הבאג שקט – אובייקט הקורוטינה נוצר, נזרק, ולעולם אינו מורץ. האפליקציה ממשיכה כאילו הכול עבד. MicroPython לעיתים ירשום אזהרה שלקורוטינה לעולם לא המתינו; לעיתים לא. בדוק שמא חסרים awaits בכל אתר קריאה שנראה כמו קריאת פונקציה.

8.15.2. לולאות צמודות ללא await

קורוטינה שרצה בלולאה ולעולם אינה מבצעת awaits מנכסת לעצמה את לולאת האירועים. אף משימה אחרת אינה מתקדמת עד שהלולאה יוצאת או מוסרת שליטה:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

התיקון הוא מסירת שליטה (yield) בתוך הלולאה – בדרך כלל await asyncio.sleep_ms(0) – כדי שמשימות מוכנות אחרות יקבלו הזדמנות לרוץ. עבודה עתירת-חישוב שייכת לאותה צורה: לולאת עיבוד תמונה שרצה במשך מאות מילישניות לכל איטרציה צריכה למסור שליטה לפחות פעם אחת לכל איטרציה כדי ששאר התוכנית לא תיתקע.

8.15.3. בליעת CancelledError

עמוד הביטול כבר כיסה זאת בפירוט. חוזרים על כך כאן מכיוון שזו הסיבה הנפוצה ביותר ל“האפליקציה שלי לא נכבית“: קורוטינה תופסת asyncio.CancelledError לצורכי ניקוי ושוכחת להעלות אותה מחדש. המשימה ממשיכה לרוץ; הקורא שביקש את הביטול תקוע לנצח בהמתנה לסיומה. תמיד העלה מחדש לאחר הניקוי, או השתמש בבלוק try/finally במקום except מפורש.

8.15.4. שינוי מצב משותף לאורך await

תזמון שיתופי מבטיח שלקורוטינה יש את ה-CPU לעצמה בין פעולות await, אך ברגע שהיא מבצעת awaits, קורוטינה אחרת יכולה לרוץ. אם שתי קורוטינות משנות את אותו מבנה נתונים בצעדים הכוללים await, פעולותיהן יכולות להשתזר בדרכים המשבשות את המבנה:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

עבור מצב שמשתנה רק בין פעולות await בתוך קורוטינה אחת, אין צורך בסנכרון. עבור מצב המשתנה לאורך await ונגיש מיותר מקורוטינה אחת, עטוף את הקטע הקריטי ב-Lock.

8.15.5. await ברמת המודול

await תקף רק בתוך גוף async def. כתיבתו ברמת המודול – מחוץ לכל קורוטינה – היא שגיאת תחביר:

# bug: not inside an async def
result = await fetch()

התיקון הוא להעביר את העבודה לקורוטינה ולקרוא לה מנקודת הכניסה asyncio.run() של התוכנית.

8.15.6. קריאות asyncio.run מרובות

ל-MicroPython יש לולאת אירועים אחת. קריאה ל-asyncio.run() פעמיים ברצף – פעם להגדרה, פעם לעבודה הראשית – עדיין משתמשת באותה לולאה. קריאה לה מתוך קורוטינה רצה היא שגיאה: הלולאה כבר רצה. שני המקרים צצים לרוב כאשר סקריפט גדל באופן אורגני והמחבר מנסה להרחיבו על ידי הוספת עוד קריאות run() במקום לקפל את העבודה החדשה לתוך ה-main הקיים.

8.15.7. שימוש ב-Event מתוך פסיקה

asyncio.Event.set() בטוחה לקריאה רק מתוך לולאת האירועים. קריאה לה ממטפל פסיקה של GPIO היא סכנת שיבוש. כדי להעיר משימה מתוך פסיקה, השתמש ב-ThreadSafeFlag במקום – העמוד עליה מכסה את הצורה.

8.15.8. קריאות סינכרוניות ארוכות

קורוטינה יכולה להמתין לפרימיטיבי ההמתנה של asyncio עצמו; כל דבר אחר שהיא קוראת רץ באופן סינכרוני וחוסם את הלולאה עד שהוא חוזר. time.sleep() חוסם של 200 מילישניות, כתיבה לכרטיס SD שלוקחת 80 מילישניות לשטיפה, דחיסת JPEG גדולה, קריאת csi.CSI.snapshot() – כל אחד מאלה מחזיק את לולאת האירועים למשך כל זמנו. התיקון תלוי בקריאה:

  • עבור time.sleep: החלף אותו ב-await asyncio.sleep או ב-await asyncio.sleep_ms.

  • עבור csi.CSI.snapshot: השתמש בעוטף תמונת הבזק האסינכרוני שעמוד הלכידה בונה.

  • עבור חישוב ארוך (עיבוד תמונה, קידוד JPEG): קבל את העלות או פצל את העבודה לחתיכות שמבצעות await בין איטרציות.

asyncio אינו יכול להפוך קריאה סינכרונית ללא-חוסמת. הוא יכול רק לאפשר לקורוטינות אחרות לרוץ בזמן שמשהו אחר ממתין.