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 会在协程的调用方已要求它停止时仍让其保持存活,这几乎总是一个 bug:
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 中冒出来的那个。