你的 asyncio 任务可能会在执行中途夭折

许多 Python 服务使用这种模式来运行后台任务:

async def create_order(order):
    await save(order)
    asyncio.create_task(send_webhook(order))
    return {"status": "created"}

目标是在不让客户端等待的情况下发送 webhook。这在开发环境中运行良好,但在生产环境中会随机失败。

问题在于垃圾回收。

当你调用 asyncio.create_task 且没有保存结果时,该任务就会变得可以被垃圾回收。事件循环仅保留对任务的弱引用。如果没有其他对象指向该任务,Python 可能会在任务仍在运行时将其回收。

你的工作就此停止。你的错误追踪器中没有任何错误。代码是完美的。任务就这样凭空消失了。

你可能会在 stderr 中看到这样的日志: Task was destroyed but it is pending!

这个错误很难发现,因为它出现的时间通常比实际故障发生的时间晚得多。

如何修复:

使用后台任务集合来持有引用。

background_tasks = set()

def fire_and_forget(coro):
    task = asyncio.create_task(coro)
    background_tasks.add(task)
    task.add_done_callback(background_tasks.discard)
    return task

该集合会让任务保持存活。回调函数会在工作完成后将其移除。这可以防止内存泄漏,并阻止垃圾回收器杀死你的任务。

对于 Python 3.11 及更高版本,你可以使用 TaskGroupsTaskGroup 会对其内部的每个任务持有强引用。请注意,TaskGroup 在退出代码块之前会等待所有任务完成。如果任务必须在响应用户之前完成,请使用此方法。

对于繁重的后台工作,请使用监督者模式:

  • 在启动时创建一个长生命周期的任务。
  • 使用队列向其发送工作。
  • 请求处理器只负责将项目添加到队列中。

这能确保你的工作得以延续,并允许你稍后添加重试机制。

在 Python 中,你必须负责管理你的任务。如果你不持有引用,运行时将不会保护它们。

来源:https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1