取消、超时与 TaskGroup
asyncio 程序经常要提前停掉一件事。用户断开连接了,请求不用继续查数据库;服务准备退出了,后台 worker 要停下来;某个接口超过 500ms 还没回来,就别等了。这些场景都会用到取消。
取消不是 kill
task.cancel()这不会把正在执行的 Python 代码立刻按停。它只是告诉这个 Task:下次你从 await 醒来时,请抛出一个 asyncio.CancelledError。
async def worker():
try:
await asyncio.sleep(10)
finally:
print("cleanup")如果这个 Task 被取消,finally 仍然会执行。取消会通过异常进入协程,协程因此还有机会收尾:关连接、释放锁、回滚事务,都可以放在这里。
别吞掉 CancelledError
坏写法:
async def worker():
try:
await do_work()
except asyncio.CancelledError:
print("cancelled")这段代码把取消吃掉了。调用方看不到 CancelledError,可能会以为任务正常结束了。通常要这样写:
async def worker():
try:
await do_work()
except asyncio.CancelledError:
print("cancelled")
raiseTaskGroup 和 asyncio.timeout() 都靠这个异常来收拾局面。如果你把它吞掉,上层就不知道这个任务到底是完成了,还是被要求停下来了。
timeout 背后用的是取消
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。外层被取消时,里面那个任务不会立刻跟着取消。它不是“忽略取消”,只是把“谁会被取消”这件事隔开。
比如:
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 解决的是这类问题:一组任务属于同一次操作,其中一个失败后,其他任务也应该停下来。规则可以这样记:
- 进入
async with后,可以创建多个子任务。 - 代码块退出时,会等待所有子任务完成。
- 任一子任务抛出非取消异常时,其他子任务会被取消。
- 如果有多个异常,会一起放进
ExceptionGroup或BaseExceptionGroup。
这比随手 create_task() 明确。代码块结束时,这一组任务也有明确结果:要么全部完成,要么失败并清理干净。
容易踩的坑
| 错误 | 后果 | 修正 |
|---|---|---|
捕获 CancelledError 后不重新抛出 | 上层以为任务正常结束 | 清理后 raise |
create_task() 后不保存引用 | 异常无人处理,生命周期失控 | 用 TaskGroup 或保存到集合 |
用 wait_for() 包住不可取消的阻塞调用 | 超时无法及时生效 | 改用异步库或放入线程池 |
在 finally 里做很慢的同步清理 | 取消响应变慢 | 清理逻辑也保持异步友好 |
小练习
运行:
python3 examples/asyncio_demos/06_timeout_cancel.py
python3 examples/asyncio_demos/07_taskgroup_failure.py然后做三个修改:
- 把
06_timeout_cancel.py里的超时时间改大,确认任务能正常完成。 - 在
06_timeout_cancel.py里去掉CancelledError后面的raise,观察外层输出。 - 在
07_taskgroup_failure.py里让search不再失败,观察三个任务是否都正常结束。