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