8.5. 逾時與取消¶
取消是 asyncio 對「停止執行這個協程、在其內部引發一個例外讓它有機會清理、並把它從排程器中移除」的稱呼。逾時是取消某項操作最常見的原因;手動的 task.cancel() 則是另一個原因。
8.5.1. 取消一個任務¶
呼叫 Task.cancel 會排程在執行中的協程下一次 await 處引發 asyncio.CancelledError。協程可以自由選擇忽略這個取消(捕捉例外並繼續執行)或者尊重它 -- 通常的選擇是在執行完清理程式碼之後尊重它:
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() 會為一個可等待物件包上一個期限。如果該可等待物件在逾時時間內完成,就會回傳其結果。如果沒有,該可等待物件會被取消,而呼叫端會收到 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 呼叫的任務被取消,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 冒出的那個。