Python asyncio 任务取消与超时处理:避免资源泄漏的实战写法
在 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 块,程序在高并发和长运行场景下才会更稳定。
- 点赞
- 收藏
- 关注作者
评论(0)