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()— 引數以秒為單位,接受浮點數。asyncio.sleep_ms()— 引數以毫秒為單位,接受整數。這是 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本身就是可等待的。result = await task會暫停當前協程,直到task的協程回傳,然後以該協程所回傳的任何值恢復執行(或重新引發它所引發的任何例外)。取消任務。
task.cancel()會排程在任務協程的下一個await處於其內部引發asyncio.CancelledError,讓它有機會在finally區塊中執行清理程式碼。逾時與取消 一頁涵蓋了相關細節。稍後識別它。
asyncio.current_task()會回傳當前正在執行的協程所對應的Task。大多數指令碼從不呼叫它;它出現在偵測工具與例外處理常式中。
指令碼不必每次都擷取控制代碼。應用程式啟動後便放任其運行的拋棄式背景任務,可以丟棄回傳值 — 迴圈仍會排程它們:
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 呼叫會排程兩個 heartbeat,而不等待其中任何一個。控制權立即返回 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 中。