asyncio 里”超时”和”取消”是一件事的两面:超时本质上是到点发取消信号。但很多代码只包了 wait_for 就以为万事大吉,真正的问题往往出在被取消的任务不肯干净退出。
最基础的写法
async def fetch(session, url):
try:
async with asyncio.timeout(5): # Python 3.11+
async with session.get(url) as resp:
return await resp.text()
except TimeoutError:
return None
asyncio.timeout() 是 3.11 推荐的方式,比 wait_for 更轻——它不会每次调用都新建 Task。
为什么 wait_for 有隐患
# 不推荐:wait_for 会取消并等待内部任务清理
await asyncio.wait_for(slow_task(), timeout=5)
wait_for 在超时会取消内层任务,但如果内层任务吞掉了 CancelledError,wait_for 会一直等到它真正结束,超时就形同虚设。
吞掉 CancelledError 的典型反模式
async def worker():
while True:
try:
await do_io()
except Exception:
pass # 错误:把 CancelledError 也吞了
CancelledError 在 3.8 之后继承自 BaseException,except Exception 抓不到它,这一点反而安全。但要小心这种写法:
except BaseException:
log.error("出错了")
return None # 错误:取消被当成了错误,循环无法退出
一旦任务被取消后没有重新抛出,asyncio 会认为它”拒绝取消”,整个关闭流程可能卡住。
正确的取消处理
async def worker():
try:
while True:
await do_io()
except asyncio.CancelledError:
await cleanup() # 释放资源
raise # 必须重新抛出
raise 那一行是关键:处理完清理工作后,把取消信号继续往上传。
批量任务的超时
抓 100 个 URL,要求整体 10 秒内完成:
async with asyncio.TaskGroup() as tg:
for url in urls:
tg.create_task(fetch(session, url))
TaskGroup 配合 asyncio.timeout() 会在超时时取消组内所有任务,并等它们清理完才抛异常,比手动管理 gather 更可靠。
小结
- 超时优先用
asyncio.timeout(),不要每次新建 Task - 捕获
CancelledError后,清理完一定要重新 raise - 批量任务用
TaskGroup,别自己拼gather+ 异常判断