8.5. פסקי זמן וביטול#
ביטול הוא השם של asyncio ל“הפסק להריץ את הקורוטינה הזו, העלה חריגה בתוכה כדי שתהיה לה הזדמנות להתנקות, והסר אותה מהמתזמן“. פסקי זמן הם הסיבה הנפוצה ביותר לבטל משהו; task.cancel() ידני הוא השני.
8.5.1. ביטול משימה#
קריאה ל-Task.cancel מתזמנת את asyncio.CancelledError להיות מועלית בתוך הקורוטינה הרצה ב-await הבא שלה. הקורוטינה חופשית להתעלם מהביטול (לתפוס את החריגה ולהמשיך לרוץ) או לכבד אותו – הבחירה הרגילה היא לכבד אותו לאחר הרצת קוד ניקוי:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
סעיף finally חשוף הוא הדפוס הנקי ביותר: הניקוי רץ בין אם הקורוטינה יצאה כרגיל, העלתה חריגה לא קשורה, או בוטלה. ה-CancelledError מתפשטת בחזרה כלפי מעלה דרך ה-finally, הלולאה רואה שהמשימה הסתיימה, והקורא של await task רואה את הביטול.
תפיסת CancelledError באופן מפורש היא גם בסדר כשהיישום רוצה לעשות איתה משהו ספציפי – לתעד אותה, למסור את המשאב בצורה נקייה וכו«. הכלל הוא: להעלות אותה מחדש לאחר שהניקוי רץ. בליעת CancelledError משאירה את הקורוטינה בחיים כשהקורא שלה ביקש ממנה לעצור, שזה כמעט תמיד באג:
async def worker():
try:
await long_running_thing()
except asyncio.CancelledError:
log("worker cancelled, cleaning up")
close_resources()
raise # << this line is the important one
8.5.2. פסקי זמן עם wait_for#
asyncio.wait_for() עוטף awaitable במועד אחרון. אם ה-awaitable מסתיים בתוך פסק הזמן, התוצאה שלו מוחזרת. אם לא, ה-awaitable מבוטל והקורא מקבל asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
ארגומנט השניות מקבל מספר עשרוני. עבור מועדים אחרונים בצורת מילישניות, asyncio.wait_for_ms() מקבל מספר שלם של מילישניות – הרחבה של MicroPython המתיישבת עם כפתורי התזמון של הקושחה בגרעיניות של מילישניות.
באופן פנימי, wait_for עושה בדיוק את מה שביטול ידני היה עושה: כשהמועד האחרון פג הוא קורא ל-cancel() על המשימה העטופה, CancelledError מועלית בתוך הקורוטינה, סעיפי finally רצים, וברגע שהניקוי הושלם החריגה מתורגמת ל-TimeoutError עבור הקורא.
משמעות הדבר שקורוטינה שתופסת ומתעלמת מ-CancelledError תסכל את פסק הזמן – המועד האחרון פג, אך הקורוטינה סירבה לעצור, ו-wait_for אינו יכול לאלץ אותה. הכלל הקודם חל גם כאן: תפוס את CancelledError רק כדי להריץ ניקוי, ואז העלה מחדש.
8.5.3. ביטול דרך gather#
ביטול מתפשט כלפי מטה דרך gather(). אם המשימה הממתינה לקריאת gather מבוטלת, כל awaitable שעדיין רץ בתוך ה-gather מבוטל גם הוא – כל אחד מקבל הזדמנות להתנקות דרך סעיפי ה-finally שלו לפני שהביטול מתגלגל כלפי מעלה אל הקורא.
בשילוב עם פסקי זמן, זוהי הדרך הסטנדרטית לשים מועד אחרון על קבוצה של פעולות:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
או שכל תת-פעולה מסתיימת בתוך חמש שניות, או שכולן מבוטלות יחד.
8.5.4. כיבוי יישום#
ביטול הוא גם הדרך שבה יישום אמיתי נעצר בצורה נקייה. הדפוס עקבי בין סקריפטים: main תופס את הידיות של משימות הרקע ארוכות-החיים שהתחיל, מריץ את העבודה ברמה העליונה, ואז מבטל כל ידית וממתין לה בבלוק finally
async def main():
sender = asyncio.create_task(uplink())
watcher = asyncio.create_task(button_watcher())
try:
await snapshot_loop()
finally:
sender.cancel()
watcher.cancel()
await asyncio.gather(sender, watcher,
return_exceptions=True)
ה-return_exceptions=True הוא הטריק שמונע מה-gather להעלות מחדש את ה-CancelledError שכל משימת ילד עומדת למסור, כך שסיבת היציאה של היישום עצמו – בין אם snapshot_loop העלה חריגה ובין אם לא – היא מה שמבעבע החוצה מ-main.