8.6. 异常¶
asyncio 脚本中的异常行为几乎与常规 Python 中相同——它们会沿调用链向上传播,直到有东西捕获它们。说几乎是因为任务是并行运行的,所以“向上”的路径并不是创建该任务的路径。本页将介绍在各种常见结构中异常去往何处。
8.6.1. 在单个协程内部¶
协程内部的 try/except 会以通常的方式捕获它所 await 的任何东西抛出的异常:
async def fetch_with_retry(url):
for attempt in range(3):
try:
return await fetch(url)
except OSError as e:
last_error = e
raise last_error
这里没有任何 asyncio 特有的内容——except 子句会看到 fetch 内部抛出的异常,就好像它是一次普通的函数调用一样。
8.6.2. 在应用程序正在等待的任务中¶
当一个作为 Task 运行的协程抛出异常时,该异常会被存储在任务上。下一次有东西 await 该任务时,异常会在该 await 处被重新抛出:
task = asyncio.create_task(may_fail())
try:
result = await task
except OSError:
log("may_fail failed")
同样的机制也适用于 asyncio.gather()。其默认行为——一个子项抛出异常、其余的被取消、异常传播出 gather——正是源自这一机制。
8.6.3. 在无人等待的任务中¶
一个从来没有人 await 的任务是需要注意的情况。异常仍然会发生;循环会注意到该任务以未处理的异常结束;但没有任何 await 可供它在此处浮现。默认行为是通过 sys.stderr 打印一条回溯信息并继续运行——对于无人值守的诊断而言这没问题,但对于一个想要知晓情况的应用程序来说就很不合适了。
正确的做法通常是等待该任务。可以直接等待,即记住其句柄并在关闭时等待它;也可以隐式地通过 gather() 或 wait_for() 等待。超时与取消 页面中的“关闭应用程序”模式会为典型脚本所生成的那些长期存在的后台任务处理这种情况。
8.6.4. 自定义异常处理程序¶
当静默地打印回溯并继续运行还不够时,循环会暴露一个钩子——Loop.set_exception_handler——应用程序可以重写它来做些别的事情:
def handler(loop, context):
print("asyncio:", context.get("message"))
if "exception" in context:
sys.print_exception(context["exception"])
loop = asyncio.get_event_loop()
loop.set_exception_handler(handler)
context 参数是一个字典,包含 'message'、'exception' 和 'future' 等键。在某些警告类事件中可能缺少异常,这就是示例使用 .get() 的原因。
典型的用途包括把故障记录到闪存、闪烁一个错误 LED,或升级为看门狗重启。循环控制 页面涵盖了循环钩子的完整内容。
8.6.5. KeyboardInterrupt¶
当脚本从外部被停止时——通常是 IDE 要求它停止——该请求会作为一个 KeyboardInterrupt 到达脚本内部。在 asyncio.run() 内部,它会像任何其他未处理的异常一样传播:main 被取消,循环正在跟踪的每个任务也都被取消,然后 KeyboardInterrupt 被重新抛出,传出 asyncio.run()。finally 子句会在退出过程中运行,因此处理它的正是取消页面中那同一个清理模式。