Skip to content

取消、超时与 TaskGroup

asyncio 程序经常要提前停掉一件事。用户断开连接了,请求不用继续查数据库;服务准备退出了,后台 worker 要停下来;某个接口超过 500ms 还没回来,就别等了。这些场景都会用到取消。

取消不是 kill

python
task.cancel()

这不会把正在执行的 Python 代码立刻按停。它只是告诉这个 Task:下次你从 await 醒来时,请抛出一个 asyncio.CancelledError

python
async def worker():
    try:
        await asyncio.sleep(10)
    finally:
        print("cleanup")

如果这个 Task 被取消,finally 仍然会执行。取消会通过异常进入协程,协程因此还有机会收尾:关连接、释放锁、回滚事务,都可以放在这里。

别吞掉 CancelledError

坏写法:

python
async def worker():
    try:
        await do_work()
    except asyncio.CancelledError:
        print("cancelled")

这段代码把取消吃掉了。调用方看不到 CancelledError,可能会以为任务正常结束了。通常要这样写:

python
async def worker():
    try:
        await do_work()
    except asyncio.CancelledError:
        print("cancelled")
        raise

TaskGroupasyncio.timeout() 都靠这个异常来收拾局面。如果你把它吞掉,上层就不知道这个任务到底是完成了,还是被要求停下来了。

timeout 背后用的是取消

python
async with asyncio.timeout(0.5):
    await fetch()

超时时会发生这些事:

注意:在 async with asyncio.timeout() 外面捕获的是内置 TimeoutError

wait_for 和 shield

asyncio.wait_for(aw, timeout) 会等待一个 awaitable,超时时取消它并抛出 TimeoutError

asyncio.shield(aw) 用来保护里面的 awaitable。外层被取消时,里面那个任务不会立刻跟着取消。它不是“忽略取消”,只是把“谁会被取消”这件事隔开。

比如:

python
task = asyncio.create_task(commit_transaction())
try:
    await asyncio.wait_for(asyncio.shield(task), timeout=1)
except TimeoutError:
    print("commit is still running in background")

这种写法要小心。commit_transaction() 可能还在后台跑,你必须保存 task,并在后面检查它有没有成功、失败或还需要取消。

TaskGroup 失败时会替你收尾

TaskGroup 解决的是这类问题:一组任务属于同一次操作,其中一个失败后,其他任务也应该停下来。规则可以这样记:

  1. 进入 async with 后,可以创建多个子任务。
  2. 代码块退出时,会等待所有子任务完成。
  3. 任一子任务抛出非取消异常时,其他子任务会被取消。
  4. 如果有多个异常,会一起放进 ExceptionGroupBaseExceptionGroup

这比随手 create_task() 明确。代码块结束时,这一组任务也有明确结果:要么全部完成,要么失败并清理干净。

容易踩的坑

错误后果修正
捕获 CancelledError 后不重新抛出上层以为任务正常结束清理后 raise
create_task() 后不保存引用异常无人处理,生命周期失控TaskGroup 或保存到集合
wait_for() 包住不可取消的阻塞调用超时无法及时生效改用异步库或放入线程池
finally 里做很慢的同步清理取消响应变慢清理逻辑也保持异步友好

小练习

运行:

bash
python3 examples/asyncio_demos/06_timeout_cancel.py
python3 examples/asyncio_demos/07_taskgroup_failure.py

然后做三个修改:

  1. 06_timeout_cancel.py 里的超时时间改大,确认任务能正常完成。
  2. 06_timeout_cancel.py 里去掉 CancelledError 后面的 raise,观察外层输出。
  3. 07_taskgroup_failure.py 里让 search 不再失败,观察三个任务是否都正常结束。

面向学习目的的 Python asyncio 中文教程与 mini_asyncio 教学运行时。