登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python asyncio.create_task 怎样保存任务引用避免提前回收

来源:17golang原创

时间:2026-11-12 09:58:23 296浏览 收藏

如果把 asyncio.create_task() 的返回值直接丢掉,后台任务并不等于“永远会跑完”。事件循环对 Task 只保留弱引用,任务还没完成时若没有其他强引用,就可能被垃圾回收。稳妥做法是把任务放进一个集合,并在完成回调中移除它;如果任务属于同一个可等待的工作组,则优先使用 asyncio.TaskGroup。

要点速览
  • create_task() 返回的 Task 必须由变量、集合或 TaskGroup 等对象继续持有。
  • 后台集合要在任务结束后清理,否则长时间运行的服务会留下越来越多引用。
  • 只保存引用不能自动处理异常;需要回调读取异常,或让 TaskGroup 统一等待与传播。

为什么 create_task 创建后还要保存任务引用

create_task(coro) 会把协程包装成 Task,并安排到正在运行的事件循环中执行。它返回的 Task 同时也是取消任务、等待结果和读取异常的入口。容易被忽略的是,事件循环不会替应用代码永久持有 Task;官方文档明确提醒,循环只保留弱引用。

因此下面这种写法缺少生命周期管理:

import asyncio

async def refresh_cache():
    # 模拟一个后台刷新动作
    await asyncio.sleep(2)

async def main():
    # 丢弃返回的 Task,缺少其他强引用
    asyncio.create_task(refresh_cache())
    await asyncio.sleep(0.1)

asyncio.run(main())

这不是说任务每次都会立刻消失,而是说它的完成不再有可靠的引用保障;同时,任务异常也没有明确的读取位置。局部变量 task 只能解决当前作用域的引用问题,服务型代码通常还需要集中管理一批后台任务。

Python asyncio.create_task、事件循环弱引用与后台任务集合强引用的关系说明图
图1:asyncio 任务引用关系说明图,展示强引用与完成后清理的边界。

用集合保存后台任务,并在完成后自动清理

适合“启动后不需要立即等待,但仍要让它可靠完成”的场景,可以把 Task 加入集合。add_done_callback() 在任务结束时触发,调用 discard 既能移除已完成任务,也不会因为回调重复执行而抛出删除错误。

import asyncio

background_tasks = set()

async def send_report(report_id: int) -> None:
    # 真实代码中这里可以替换成异步网络或文件操作
    await asyncio.sleep(0.2)
    print(f"report {report_id} finished")

def observe_task(task: asyncio.Task) -> None:
    # 先处理取消,避免把正常取消误报成业务失败
    if task.cancelled():
        return
    # 读取异常,让后台失败进入可观察路径
    error = task.exception()
    if error is not None:
        print(f"background task failed: {error!r}")

async def start_reports() -> None:
    for report_id in range(3):
        task = asyncio.create_task(send_report(report_id))
        # 集合提供强引用,避免任务只剩事件循环弱引用
        background_tasks.add(task)
        task.add_done_callback(background_tasks.discard)
        task.add_done_callback(observe_task)

    # 示例只等待集合变空;生产代码可由服务生命周期统一收尾
    while background_tasks:
        await asyncio.sleep(0.05)+
asyncio.run(start_reports())

这里有两个不同职责:集合负责“任务还活着时有人持有它”,回调负责“任务结束时释放引用并读取异常”。如果任务执行时间很长,集合大小就是当前未完成任务数,可作为简单的运行状态指标。

后台任务怎样把异常变成可观察结果

任务完成并不代表成功。若回调中直接调用 task.exception(),必须先判断 task.cancelled(),否则被取消的任务会再次抛出 CancelledError。普通异常则可以记录日志、写入指标或交给上层处理。

需要注意,读取异常不会让业务自动重试,也不会替代资源清理;协程内部仍应使用 try/finally 释放连接、锁或临时文件。若后台任务失败后没有任何地方读取异常,最终可能出现 Task exception was never retrieved,这通常说明异常观察链路缺失。

Python asyncio 后台任务完成、取消、读取异常与 TaskGroup 统一传播的结构图
图2:后台任务异常观察结构图,区分取消、失败和 TaskGroup 的统一传播。

什么时候改用 TaskGroup

如果一组任务属于同一个业务操作,而且调用方需要等待它们全部结束,asyncio.TaskGroup 通常比手动维护集合更合适。它会持有组内任务,在上下文退出时等待;当某个任务出现非取消异常时,会取消同组剩余任务并把异常集中传播给调用方。

import asyncio

async def rebuild_index(shard: int) -> None:
    # 每个分片独立执行;异常由 TaskGroup 统一处理
    await asyncio.sleep(0.1)
    if shard == 2:
        raise RuntimeError("shard 2 failed")

async def rebuild_all() -> None:
    try:
        async with asyncio.TaskGroup() as group:
            for shard in range(4):
                # TaskGroup 负责持有、等待和传播异常
                group.create_task(rebuild_index(shard))
    except* RuntimeError as errors:
        # 这里可以记录 ExceptionGroup 中的业务错误
        print(f"rebuild failed: {len(errors.exceptions)}")

asyncio.run(rebuild_all())
场景推荐方式要确认的边界
独立后台动作,不立即等待集合 + done callback强引用、异常读取、结束清理
同一业务操作,需要统一等待TaskGroup异常会影响同组任务,需设计取消语义
调用方必须拿到结果保存 Task 并 await不要把结果型任务伪装成 fire-and-forget

相关问题

只用一个局部变量保存 Task 可以吗

短函数里可以,但变量离开作用域后就没有稳定的强引用;服务中的后台任务更适合使用集合或 TaskGroup。

为什么集合不能一直保存已完成任务

已完成 Task 仍被集合引用,长时间运行会让集合持续增长,所以应使用 add_done_callback(background_tasks.discard) 自动清理。

TaskGroup 能不能替代所有 create_task

不能。需要独立后台生命周期时,手动集合更贴近需求;TaskGroup 适合有共同边界、需要等待和统一异常传播的一组任务。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>