task.cancel() 之后还在跑:asyncio 超时任务的三层收尾

举报
yd_210306688 发表于 2026/11/05 09:25:47 2026/11/05
【摘要】 从 asyncio 超时后的取消传播入手,说明如何避免后台任务和连接泄漏,并给出资源所有权、取消等待与上线检查清单。

task.cancel() 之后还在跑:asyncio 超时任务的三层收尾

在异步服务里,给一次调用加上 timeout 很容易,但“超时之后任务已经真正结束”并不自动成立。常见的线上现象是:请求在 3 秒后返回 504,后台却仍有一个协程占着数据库连接;或者用户连续重试,旧任务没有被回收,新的任务又在同一资源上排队。问题往往不在等待本身,而在取消后的收尾。

一、超时只是发出取消请求

以 Python 3.11 的 asyncio.timeout 为例,超时到达时,事件循环会在当前任务内抛出 CancelledError。业务代码如果吞掉这个异常,外层就会误以为操作已经停止。需要区分“业务失败”和“任务被取消”:前者可以转换成错误响应,后者通常必须继续向上传播。

async with asyncio.timeout(3):
    data = await fetch(url)

不要捕获 Exception 后统一返回默认值,因为这会破坏取消语义;如果要处理 CancelledError,通常只做必要的记录,然后原样 raise。

二、清理阶段必须可等待

真正容易遗漏的是 finally 里的异步清理。关闭连接、释放锁、删除临时文件都要允许等待完成;但清理代码本身不应无限等待。可以为清理动作设置更短的截止时间,同时避免在取消处理中又发出大量新请求。

当多个任务共享连接池时,建议由每一层只管理自己创建的资源。上层的超时不能替代下层的关闭;下层也不应因为一次取消就破坏仍被其他任务使用的共享对象。资源所有权清楚,排查时才能回答“这个连接是谁创建、由谁释放”。

三、不要让 shield 变成后台泄漏

有些代码用 asyncio.shield() 保护工作协程,避免等待方被取消时打断真正的写入。但保护并不等于可以忘记它。更稳妥的写法是:超时后显式取消工作协程,并等到它退出后再返回。

worker = asyncio.create_task(job.run())
try:
    await asyncio.wait_for(asyncio.shield(worker), timeout=5)
except TimeoutError:
    worker.cancel()
    with contextlib.suppress(asyncio.CancelledError):
        await worker
    raise

这段代码的关键不是处理超时,而是最后的 await worker。它确认取消已经落地,避免接口已经失败、后台任务却继续读写外部系统。

四、上线前的检查项

发布异步任务前,可以逐项确认:超时是否覆盖最外层调用;业务异常是否会误吞取消;finally 中的清理是否有独立边界;共享资源是否只有一个所有者;取消后是否真正 await 到结束;日志是否同时记录任务 ID、开始时间、超时时间和清理结果。

超时控制的目标不是尽快返回错误,而是尽快让整条执行链进入可预测的稳定状态。把取消传播、资源清理和后台任务回收一起设计,才能避免“请求已经超时,工作还在继续”的隐性泄漏。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。