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 子句会在退出过程中运行,因此处理它的正是取消页面中那同一个清理模式。