8.2. קורוטינות ומשימות¶
קורוטינות הן יחידת העבודה שממנה נבנית תוכנית asyncio; משימות הן הדרך שבה אפליקציה מריצה מספר קורוטינות במקביל.
8.2.1. קורוטינות¶
קורוטינה היא פונקציה המוכרזת באמצעות async def
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
הגוף נראה כמו פונקציה רגילה, עם מרכיב נוסף אחד: await. בכל מקום שבו הקורוטינה צריכה להמתין למשהו – שינה, קריאה מרשת, אירוע שנקבע – היא מבצעת await לביטוי שיודע כיצד להשהות את הקורוטינה עד שהדבר שהיא ממתינה לו מוכן. בכל await הקורוטינה מחזירה את השליטה ל-asyncio; asyncio מחדש אותה מאותה נקודה ברגע שהפעולה שעליה המתינה הושלמה.
מודול asyncio מספק שתי קריאות שינה:
asyncio.sleep()– ארגומנט בשניות, מקבל float.asyncio.sleep_ms()– ארגומנט באלפיות שנייה, מקבל int. הרחבה של MicroPython; בדרך כלל הבחירה הנכונה במצלמה משום שכפתורי התזמון בקושחה הם בקנה מידה של אלפיות שנייה.
async def חשוף אינו עושה דבר בפני עצמו. קריאה ל-heartbeat(500) אינה מריצה את הגוף; היא מחזירה אובייקט קורוטינה ש-asyncio צריך לתזמן. הדרך הפשוטה ביותר לתזמן אחד היא asyncio.run()
asyncio.run(heartbeat(500))
asyncio.run() מפעיל את לולאת האירועים, מתזמן את הקורוטינה שנמסרה לו כנקודת הכניסה ברמה העליונה, מריץ את הלולאה עד שאותה קורוטינה מחזירה ערך, ואז מפרק את הלולאה. עבור קורוטינה יחידה זו כל התוכנית. עבור מספר קורוטינות האפליקציה פונה למשימות.
8.2.2. משימות¶
משימה היא העטיפה של asyncio סביב קורוטינה שאומרת תזמן אותה במקביל לנוכחית ותן לי להמשיך. asyncio.create_task() יוצר אחת ומחזיר אובייקט Task המייצג את העבודה המתוזמנת:
task = asyncio.create_task(heartbeat("fast", 100))
הקורוטינה נמצאת כעת בלוח הזמנים של הלולאה; הקורא לא המתין לה. אובייקט ה-Task המוחזר הוא הידית שבה הקורא משתמש לאחר מכן כדי לקיים אינטראקציה עם אותה עבודה רצה.
ברגע שלאפליקציה יש את הידית היא יכולה לעשות בה שלושה דברים:
להמתין לסיום המשימה. אובייקט
Taskהוא בעצמו ניתן-להמתנה (awaitable).result = await taskמשהה את הקורוטינה הנוכחית עד שהקורוטינה שלtaskמחזירה ערך, ואז מתחדשת עם הערך שאותה קורוטינה החזירה (או זורקת מחדש את מה שהיא זרקה).לבטל את המשימה.
task.cancel()מתזמן אתasyncio.CancelledErrorלהיזרק בתוך הקורוטינה של המשימה ב-awaitהבא שלה, ובכך נותן לה הזדמנות להריץ קוד ניקוי בבלוקfinally. העמוד על פסקי זמן וביטול מכסה את הפרטים.לזהות אותה מאוחר יותר.
asyncio.current_task()מחזירה את ה-Taskעבור הקורוטינה שרצה כרגע. רוב הסקריפטים לעולם אינם קוראים לה; היא מופיעה במכשור (instrumentation) ובמטפלי חריגות.
הסקריפט אינו חייב לתפוס את הידית בכל פעם. משימות רקע חד-פעמיות שהאפליקציה מפעילה ומשאירה לרוץ יכולות לוותר על ערך ההחזרה – הלולאה עדיין מתזמנת אותן:
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
שתי הקריאות ל-create_task מתזמנות את שני ה-heartbeats מבלי להמתין לאף אחד מהם. השליטה חוזרת מיד אל main, שאז מבצע await לשינה של חמש שניות. בעוד הוא ישן, שתי משימות ה-heartbeat מתקדמות; הלולאה עוברת במחזור על כל משימה שמוכנה לרוץ. לאחר חמש שניות main מחזיר ערך, הלולאה מפרקת את כל המשימות שעדיין חיות, ו-asyncio.run() חוזר אל הקורא.
תפסו את הידית בכל פעם שהאפליקציה באמת זקוקה לאחת משלוש הפעולות לעיל. בפועל זה אומר כמעט תמיד, משום שכיבוי נקי של אפליקציה משמעו ביטול משימות הרקע שהיא יצרה – עמוד הביטול מכסה את התבנית.
8.2.3. כלל שתי השורות¶
תוכנית ה-asyncio המינימלית היא שתי השורות שבהן מסתיימות הדוגמאות לעיל:
async def main():
...
asyncio.run(main())
כל השאר – המשימות שהאפליקציה יוצרת, הפרימיטיבים שבהם היא מתאמת ביניהן, הזרמים שהיא פותחת – מתרחש בתוך main (ובתוך הקורוטינות ש-main מפיק). כאשר סקריפט מתעלה על לולאת ה-while True: csi0.snapshot() הקלאסית של המצלמה, התשובה אינה לקרוא ל-asyncio.run() בכמה מקומות; היא לקפל את העבודה החדשה אל main כמשימות נוספות.