你的 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 及更高版本,你可以使用 TaskGroups。TaskGroup 会对其内部的每个任务持有强引用。请注意,TaskGroup 在退出代码块之前会等待所有任务完成。如果任务必须在响应用户之前完成,请使用此方法。
对于繁重的后台工作,请使用监督者模式:
- 在启动时创建一个长生命周期的任务。
- 使用队列向其发送工作。
- 请求处理器只负责将项目添加到队列中。
这能确保你的工作得以延续,并允许你稍后添加重试机制。
在 Python 中,你必须负责管理你的任务。如果你不持有引用,运行时将不会保护它们。
来源:https://dev.to/r9v/your-asyncio-task-can-be-garbage-collected-mid-flight-3kg1
