Python asyncio 任务取消与超时处理:避免资源泄漏的实战写法

举报
yd_257599996 发表于 2026/10/31 06:30:14 2026/10/31
【摘要】 本文讲解 Python asyncio 中任务取消与超时的协作机制,重点说明 finally 资源释放、子任务收尾和 shield 的适用边界,并给出可直接套用的检查清单。

在 Python 的 asyncio 程序里,超时和取消是最容易写出资源泄漏的地方。表面上看,任务被取消了,程序也继续运行了,但底层的连接、文件句柄、子任务可能还留在后台。本文用几个可复现的写法,说明取消到底发生了什么,以及怎样收尾才干净。

一、取消不是立即终止

调用 task.cancel() 之后,任务并不会在任意一行代码处立刻停下。它只是在任务当前等待的 await 点抛出 CancelledError。如果任务正在执行一段没有 await 的纯计算,或者已经捕获了所有异常却没有重新抛出,取消就可能被吞掉。

因此,取消处理的第一条规则是:不要无条件吞掉 CancelledError。需要记录日志、释放资源,然后让异常继续向上传播,或者在明确知道任务已经结束的情况下做收尾。

二、用 wait_for 做超时

asyncio.wait_for 是最常用的超时写法。它会在超时后取消内部任务,并把超时转换为 TimeoutError。下面的模式适合调用外部接口、读写数据库、等待队列等场景:

import asyncio

async def fetch_data():
    await asyncio.sleep(10)
    return {"status": "ok"}

async def main():
    try:
        result = await asyncio.wait_for(fetch_data(), timeout=2)
    except TimeoutError:
        print("请求超时,已放弃等待")
        return None
    return result

这里有一个容易被忽略的细节:超时后,fetch_data 内部的 await 点同样会收到取消请求。如果它在 finally 里做了清理,清理逻辑会先执行,然后 wait_for 才返回超时结果。不要在超时分支里假设内部任务已经瞬间消失。

三、在 finally 里释放资源

无论任务是正常结束、超时还是被取消,finally 都是最可靠的收尾位置。关闭连接、关闭文件、注销监听、释放信号量,都应该写在这里,而不是只写在正常分支里。

import asyncio

async def worker():
    print("开始工作")
    try:
        await asyncio.sleep(30)
    except asyncio.CancelledError:
        print("收到取消请求,开始清理")
        raise
    finally:
        print("释放资源:关闭连接、清理临时状态")

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("主流程确认任务已取消")

注意,worker 里先捕获 CancelledError 记录日志,再用 raise 继续抛出,最后由 finally 释放资源。这个顺序能保证日志、清理、取消状态都不丢失。

四、不要让子任务变成孤儿

一个常见的泄漏是:父任务被取消了,但它创建的子任务还在运行。子任务持有连接或者不断重试,程序退出时还会看到警告。更稳妥的做法是,在父任务退出前显式取消并等待子任务。

import asyncio

async def child(name):
    try:
        while True:
            await asyncio.sleep(1)
            print(name, "还在运行")
    except asyncio.CancelledError:
        print(name, "被取消")
        raise

async def parent():
    tasks = [asyncio.create_task(child("A")), asyncio.create_task(child("B"))]
    try:
        await asyncio.gather(*tasks)
    finally:
        for task in tasks:
            task.cancel()
        await asyncio.gather(*tasks, return_exceptions=True)

return_exceptions=True 的作用,是让等待子任务的过程不被第一个取消异常打断,保证每个子任务都有机会执行自己的 finally。

五、需要继续完成的关键收尾,可以用 shield

有些收尾操作本身不应该被取消打断,例如写一条完成日志、释放分布式锁、关闭事务。asyncio.shield 可以保护这段代码不被外层取消直接中断,但它并不能保证程序退出时一定等它跑完,主流程仍然需要在合适的位置 await 它。

六、一个可直接套用的检查清单

第一,超时优先用 wait_for 或者 asyncio.timeout,而不是自己写一个计时循环去轮询任务状态。

第二,凡是打开了外部资源,都要在 finally 里关闭,不要只依赖正常分支。

第三,捕获 CancelledError 后,记录必要的上下文,然后重新抛出,除非你已经明确接管了任务的结束状态。

第四,创建子任务的地方,就要同时想好它的取消和等待路径,避免孤儿任务。

第五,对取消后的状态做一次验证:连接是否关闭、队列里是否还有残留、日志里是否有未完成的任务警告。

七、总结

asyncio 的取消是一种协作机制,不是强制杀死。它依赖 await 点、异常传播和 finally 收尾三者配合。把超时、取消、资源释放当成同一件事来设计,而不是事后补一个 try 块,程序在高并发和长运行场景下才会更稳定。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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