תכנות אסינכרוני
אנחנו רוצים שהלוח יהבהב, יעקוב אחרי כפתור, יענה לדפדפן, וכל זאת בבת אחת. אבל יש לו תוכנית אחת שרצה שורה אחר שורה. בפרק 4 פתרנו בעיה קטנה כזאת בעזרת מונה זמן לכל משימה, ופתרון כזה מסתבך מהר. כאן נלמד את הכלי שנוצר בשביל זה: משימות שמוותרות על התור כשהן ממתינות, וכך נראה שהן רצות בו-זמנית.
- נסביר למה פקודות חוסמות משביתות את כל מה שסביבן.
- נכתוב משימות בעזרת
asyncו-await, ונריץ אותן בפונקציותasyncio.run,gatherו-create_task. - נחשב את הזמן הכולל ונחזה את סדר האירועים בפלט.
- נקשור שתי משימות בעזרת המחלקה
Event. - נבנה מערכת שיש בה נורית, כפתור, חיישן ושרת.
- נימנע משתי המלכודות: פקודה חוסמת בתוך משימה, ומשימה נשכחת.
- נבחר בין פסיקה, טיימר ו-
asyncioלפי הבעיה.
19.1 הבעיה: המתנה שעוצרת הכול (Blocking)
פקודה כמו time.sleep(1) היא פקודה חוסמת: כל עוד היא ממתינה, שום שורה אחרת אינה מתבצעת. נלחץ על הכפתור למשך 0.3 s בתוכנית שמהבהבת כל שנייה:
import time
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
t0 = time.ticks_ms()
for i in range(4):
led.value(1 - led.value())
time.sleep(1)
now = time.ticks_diff(time.ticks_ms(), t0) / 1000
print("%.1f s button %d" % (now, btn.value()))1.0 s button 1 2.0 s button 1 3.0 s button 1 4.0 s button 1
התוכנית קוראת את הכפתור רק בכל שנייה שלמה, והלחיצה נפלה בין שתי קריאות. שימו לב שרוב זמנו של הלוח הולך כאן על המתנה שאינה דורשת שום חישוב. כל הרעיון במשפט אחד: מי שממתין מוותר על התור לאחרים.
19.2 משימה: async ו-await (Coroutines)
await, ושם היא מחזירה את התור.asyncio ששומר רשימה של משימות ושל זמן ההתעוררות של כל אחת, ונותן את התור למשימה המוכנה.| פקודה | מה היא עושה |
|---|---|
| async def f(): | פונקציה אסינכרונית. קריאה אליה אינה מריצה אותה, אלא נותנת לנו אובייקט שממתינים לו. |
| await x | ממתינים ל-x ומוותרים על התור בינתיים. |
| await asyncio.sleep(s) | המתנה שאינה חוסמת: המשימה הזאת ישנה, ואחרות פועלות. |
| asyncio.run(main()) | מפעיל את המתזמן עד שהפונקציה main מסתיימת. |
| asyncio.gather(a, b) | מריץ את המשימות יחד, מסתיים כשכולן מסתיימות, ומחזיר רשימה של תוצאותיהן. |
import asyncio
import time
t0 = time.ticks_ms()
def stamp():
return time.ticks_diff(time.ticks_ms(), t0) / 1000
async def work(name, n, gap):
for i in range(n):
print("%.1f %s %d" % (stamp(), name, i))
await asyncio.sleep(gap)
async def main():
await asyncio.gather(work("A", 3, 1.0), work("B", 3, 0.6))
print("%.1f done" % stamp())
asyncio.run(main())0.0 A 0 0.0 B 0 0.6 B 1 1.0 A 1 1.2 B 2 2.0 A 2 3.0 done
כש-A מגיעה ל-await asyncio.sleep, תורה של B מתחיל, ובשעה 0.6 s B מתעוררת בלי להזדקק לדבר מ-A. אין כאן ביצוע מקבילי אמיתי: בכל רגע רצה שורה אחת בלבד.
| סמל | פירוש | יחידה |
|---|---|---|
| TA | סכום זמני ההמתנה של המשימה הראשונה | s |
| TB | סכום זמני ההמתנה של המשימה השנייה | s |
gather?וזו השורה האחרונה בפלט התוכנית. זמני החישוב בין שתי ההמתנות קטנים, ולכן מזניחים אותם.
B ל-1.5 s. מתי שתי המשימות מסתיימות?19.3 יצירת משימה: create_task (create_task)
הפונקציה gather ממתינה לכולם. לפעמים רוצים להפעיל משימה ולהמשיך בעבודה, ולהמתין לה מאוחר יותר או לא להמתין לה כלל. זו העבודה של create_task. וכאן הלקח החשוב ביותר על הסדר:
A אינה מתחילה ברגע יצירתה, אלא ב-await הראשון ב-main.import asyncio
async def job(name):
print(name, "starts")
await asyncio.sleep(1)
print(name, "ends")
async def main():
t = asyncio.create_task(job("A"))
print("main: task created")
await asyncio.sleep(0.1)
print("main: back after 0.1 s")
await t
print("main: A finished")
asyncio.run(main())main: task created A starts main: back after 0.1 s A ends main: A finished
- הפונקציה
create_taskרק מכניסה את המשימה לרשימה של המתזמן. והמשימה הרצה מתחלפת רק ב-await. - לכן משימה אינה קוטעת משימה אחרת באמצע שורה, ואפשר לקרוא משתנה משותף ולכתוב אותו בין שתי המתנות בלי שום מנעול.
- כדי לעצור משימה לפני סופה משתמשים ב-
t.cancel(), והוא מעלה בתוכה את החריגCancelledError.
await asyncio.sleep(0.1), מהי השורה השנייה שהתוכנית מדפיסה? ולמה?19.4 משימה אחת מעירה משימה אחרת: Event (Event)
משימה אחת עוקבת אחרי הכפתור, ומשימה אחרת מבצעת את העבודה. איך הראשונה מודיעה לשנייה בלי לשאול כל הזמן? בעזרת אירוע: אובייקט שיש בו דגל אחד, set() מרים אותו, wait() ממתין לו ו-clear() מוריד אותו.
import asyncio
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
pressed = asyncio.Event()
async def watch_button():
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
pressed.set()
last = now
await asyncio.sleep_ms(20)
async def worker():
while True:
await pressed.wait()
pressed.clear()
led.value(1 - led.value())
print("press: led", led.value())
async def main():
await asyncio.gather(watch_button(), worker())
asyncio.run(main())press: led 1 press: led 0 press: led 1 ...
pressed.clear(), הדגל נשאר מורם, ולכן wait() כבר אינה ממתינה, והעובד מסתובב בלי הפסקה.Event: התיעוד אומר ש-set() אינו בטוח לשימוש מתוך פסיקה, ומפנה אותנו למחלקה asyncio.ThreadSafeFlag. הסימולציה בספר הזה אינה מספקת אותה.worker, בזמן שהיא הייתה עסוקה. כמה פעמים תתבצע העבודה?19.5 נורית, כפתור, חיישן ושרת
שלוש משימות, לכל אחת מקצב משלה, ושני מונים שמשותפים להן. אף משימה אינה יודעת דבר על המקצב של האחרת, וזה בדיוק מה שהיה קשה עם המונים של פרק 4:
import asyncio
from machine import Pin
import board
from convert import to_percent
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
pot = board.analog_in(board.POT)
blinks = 0
presses = 0
async def blink():
global blinks
while True:
led.value(1 - led.value())
blinks += 1
await asyncio.sleep_ms(300)
async def button():
global presses
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
presses += 1
last = now
await asyncio.sleep_ms(20)
async def report():
while True:
await asyncio.sleep(2)
level = to_percent(pot.read_u16())
print("blinks", blinks, "presses", presses, "pot", level)
async def main():
await asyncio.gather(blink(), button(), report())
asyncio.run(main())blinks 7 presses 2 pot 50 blinks 14 presses 3 pot 50 ...
והשרת הוא משימה נוספת. הספרייה נותנת לנו asyncio.start_server, והיא קוראת לפונקציה בעבור כל דפדפן שמתחבר, וכל פונקציה כזאת היא משימה עצמאית. התוכנית הבאה לא הורצה בזמן בניית הספר, כי הסימולציה שלנו אינה מספקת את הפונקציה הזאת, ובדקנו אותה בהידור בלבד:
import asyncio
from machine import Pin
import board
import wifi
led = Pin(board.LED, Pin.OUT)
blinks = 0
async def blink():
global blinks
while True:
led.value(1 - led.value())
blinks += 1
await asyncio.sleep_ms(300)
async def handle(reader, writer):
await reader.readline()
while await reader.readline() not in (b"\r\n", b""):
pass
writer.write(b"HTTP/1.1 200 OK\r\n\r\n")
writer.write(b"blinks %d\n" % blinks)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
wifi.connect()
await asyncio.start_server(handle, "0.0.0.0", 80)
await blink()
asyncio.run(main())- השרת בפרק 14 ממתין ב-
accept()ואינו עושה דבר אחר. כאן ההבהוב נמשך בזמן שהשרת ממתין. await reader.readline()ו-await writer.drain()הם שני המקומות שבהם השרת מוסר את התור.wifi.connectחוסמת, אבל היא פועלת לפני שהמשימות האחרות מתחילות, ולכן אינה משביתה דבר.
async_system.py: כמה הבהובים צפויים ב-2 s? השוו לפלט.19.6 שתי מלכודות (Pitfalls)
פקודה חוסמת בתוך משימה:
המשימה slow משתמשת כאן ב-time.sleep במקום ב-await asyncio.sleep:
import asyncio
import time
t0 = time.ticks_ms()
def stamp():
return time.ticks_diff(time.ticks_ms(), t0) / 1000
async def beat():
for i in range(6):
print("%.1f beat" % stamp())
await asyncio.sleep(0.5)
async def slow():
await asyncio.sleep(1.2)
print("%.1f slow starts" % stamp())
time.sleep(2)
print("%.1f slow ends" % stamp())
async def main():
await asyncio.gather(beat(), slow())
asyncio.run(main())0.0 beat 0.5 beat 1.0 beat 1.2 slow starts 3.2 slow ends 3.2 beat 3.7 beat 4.2 beat
המתזמן אינו קוטע אף משימה: הוא ממתין עד שתחזיר את התור. ואם נחליף את השורה ב-await asyncio.sleep(2), הפעימות יחזרו למקצב שלהן:
0.0 beat 0.5 beat 1.0 beat 1.2 slow starts 1.5 beat 2.0 beat 2.5 beat 3.2 slow ends
requests.get מפרקים 15 ו-18, accept(), ולולאת while True בלי await. ואם הלולאה ארוכה ואין ברירה, מכניסים לתוכה await asyncio.sleep(0) כדי שתוותר על התור.משימה נשכחת:
A אינה רצה כלל, ו-B מתחילה ואינה מסתיימת. ואין שום הודעת שגיאה.import asyncio
async def job(name):
print(name, "starts")
await asyncio.sleep(1)
print(name, "ends")
async def main():
a = job("A")
asyncio.create_task(job("B"))
await asyncio.sleep(0.5)
print("main ends")
asyncio.run(main())
print("program ends")B starts main ends program ends
a = job("A"): הקריאה יוצרת אובייקט בלבד, בין אם שמרנו אותו ובין אם לא. צריךawaitאוcreate_task. וזו הטעות שמבצעים הכי הרבה בהתחלה.asyncio.runמסתיים עם סיוםmain: והוא אינו ממתין לשאר המשימות. לכןmainמסתיימת ב-gatherאו ב-await tכשחשוב לנו שהן יושלמו.- משימה שמעלה חריג: מתה לבדה, והשאר ממשיכות. מה שעלול להיכשל עוטפים ב-
try/exceptבתוך המשימה, כמו בבוט של פרק 18.
blink הפסיקה להבהב אחרי שהוספנו לה שורה שקוראת מ-UART בפקודה uart.readline() שממתינה לנתונים. מהי הסיבה האפשרית?19.7 באיזה כלי משתמשים מתי?
| הכלי | מתי הוא פועל | מתאים ל | מגבלות |
|---|---|---|---|
| פסיקה (פרק 5) | מיד עם האירוע, וקוטעת את התוכנית | אירוע קצר שאסור שיאבד: פולס, קצה | מטפל קצר מאוד, ואין המתנה בתוכו |
| טיימר (פרק 4) | במחזור קבוע, וקוטע את התוכנית | עבודה קצרה סדירה: קריאה או הבהוב | מטפל קצר, ומספר הטיימרים מוגבל |
ticks_diff בלולאה | כשהלולאה מגיעה אליו | משימה אחת או שתיים פשוטות | מסתבך עם כל משימה חדשה |
asyncio | כשהמשימה מחזירה את התור | הרבה משימות איטיות שממתינות: רשת, כפתור, מסך | פקודה חוסמת אחת משביתה את כולן |
והכלל המעשי: זמן התגובה ב-asyncio אינו קטן מן הקטע הארוך ביותר בין await ל-await. אם האירוע קצר מכך, הוא שייך לפסיקה, והפסיקה יכולה לרשום אותו במשתנה שמשימה תקרא אחר כך. לא פעם מחברים כמה כלים בתוכנית אחת.
_thread, ואיננו משתמשים בו בספר הזה משלוש סיבות: התיעוד מתאר אותו כניסיוני מאוד וקובע שהממשק שלו עדיין לא התייצב; חוטים יכולים להתחלף בכל שורה, ולכן נתונים משותפים דורשים מנעולים ומתגלות תקלות שקשה לשחזר; והסימולציה שלנו אינה מריצה אותו, ואילו כל תוכנית בספר הזה נבדקת בהרצה.עיקרי הפרק:
- המשימות מתחלפות על מעבד אחד, והתור מתחלף רק ב-
await. - עם
gatherהזמן הכולל הוא הארוך שבין המשימות, ולא סכומן. create_taskמפעיל משימה שמתחילה רק ב-awaitהראשון במשימה הרצה.Eventהוא דגל שמתריע למשימה, ומורידים אותו ב-clear().- פקודה חוסמת אחת עוצרת את כל המשימות, וקריאה בלי
awaitאינה מריצה דבר. - פסיקה היא לאירוע קצר, ו-
asyncioלהמתנה ארוכה.
סיכום הכללים בפרק:
| הכלל | הנוסחה |
|---|---|
| הזמן הכולל ברצף | Tseq = TA + TB |
| הזמן הכולל עם gather | T = max(TA, TB) |
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| asyncio.run(main()) | מפעיל את המתזמן עד שהפונקציה main מסתיימת. |
| await asyncio.sleep(s) | שינה שאינה חוסמת; ועמה sleep_ms. |
| asyncio.gather(a, b) | ממתין לכולם ומחזיר רשימה של התוצאות. |
| asyncio.create_task(c) | מפעיל משימה ומחזיר אובייקט שאפשר להמתין לו או לבטל אותו. |
| t.cancel() | מבטל את המשימה. |
| asyncio.Event() | דגל, ולו set, wait ו-clear. |
| asyncio.start_server(f, host, port) | שרת שקורא ל-f(reader, writer) בכל חיבור. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| תכנות אסינכרוני | Asynchronous Programming | البرمجة غير المتزامنة |
| משימה | Task | مهمة |
| מתזמן | Scheduler | المجدول |
| פקודה חוסמת | Blocking Call | أمر حاجب |
| אירוע | Event | حدث |
main: back after 0.1 s ולא A starts, כי המשימה A אינה מקבלת תור לפני ה-await הראשון ב-main, ועכשיו זה await t.Event הוא דגל ולא מונה.שאלות לתרגול:
time.sleep(1) ל-await asyncio.sleep(1)?gather, וזמניהן 1 s, 4 s ו-2.5 s. מה הזמן הכולל? ומה הוא ברצף?asyncio.run(main()) אם main קוראת ל-job() בלי await ואין בה דבר אחר?LED כל 500 ms ו-LED_EXT כל 130 ms, בפונקציה אחת.import asyncio
from machine import Pin
import board
async def blink(pin, ms):
led = Pin(pin, Pin.OUT)
while True:
led.value(1 - led.value())
await asyncio.sleep_ms(ms)
async def main():
await asyncio.gather(blink(board.LED, 500),
blink(board.LED_EXT, 130))
asyncio.run(main())async_first.py משנים את המרווח של A ל-0.5 s. סדרו את שורות הפלט לפני ההרצה.0.0 B 0
0.5 A 1
0.6 B 1
1.0 A 2
1.2 B 2
1.8 done
async_system.py מוחקים את await asyncio.sleep_ms(20) מן המשימה button. מה קורה?import asyncio
from machine import Pin
import board
led = Pin(board.LED, Pin.OUT)
btn = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
period = 500
async def blink():
while True:
led.value(1 - led.value())
await asyncio.sleep_ms(period)
async def button():
global period
last = 1
while True:
now = btn.value()
if now == 0 and last == 1:
period = 100 if period == 500 else 500
last = now
await asyncio.sleep_ms(20)
async def main():
await asyncio.gather(blink(), button())
asyncio.run(main())await, ולכן הערך אינו נכתב ונקרא באותו רגע.requests חוסמת. מקבלים זאת ומאריכים את זמן התשאול, או מעבירים את ההבהוב לטיימר ובודקים את התוצאה בלוח, כי זמן הבקשה האמיתי אינו ידוע מן הסימולציה.