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.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 אינו יכול להפוך קריאה סינכרונית ללא-חוסמת. הוא יכול רק לאפשר לקורוטינות אחרות לרוץ בזמן שמשהו אחר ממתין.