[Python]请不要再直接用 asyncio.create_task(task) 创建后台任务啦!
ProgramRipper
2022年06月25日 08:00
收录于文集
共1篇

想必很多 asyncio 教程中,都介绍了一个用法:

代码块
Python
自动换行
复制代码
import asyncio

async def background_task():
  # do something...
  ...
 
asyncio.create_task(background_task())
复制成功

asyncio 会自动后台调度运行 asyncio.Task,因此可以很方便的通过 asyncio.create_task 创建一个后台任务。但是,2022年4月25日,python/cpython 的一个不起眼的 issue (python/cpython#91887) 指出了一个问题%5E1%20

https://github.com/python/cpython/issues/91887

简而言之,asyncio 仅仅会保留对 Task 的“弱引用”(weakref)。而弱引用与我们熟知的强引用(如:赋值 a=1,列表、集合等容器内包含 [1, 2], {1, 2})有一个重要的不同,就是:弱引用不会阻止对象被 Python 的垃圾回收机制回收。

也就是说,一个未完成的,甚至是正在运行的 Task,有可能被垃圾回收中断并且清除。这不仅会让你的后台任务意外终止,还有可能影响后台任务的资源回收(因为在被垃圾收集时,代码可以被运行在任意上下文中%5E2%20)。

为了避免这些复杂后果,我们应该做的是,不要直接使用 create_task(task()) 创建后台任务,而是使用如下方式%5E3

代码块
Python
自动换行
复制代码
# 如果只有一个 task
task = asyncio.create_task(background_task()) # 通过一个全局变量,保持对 task 的强引用


# 或者,如果有多个 task
background_tasks = set()

for i in range(10):
    task = asyncio.create_task(some_coro(param=i))

    # 将 task 添加到集合中,以保持强引用:
    background_tasks.add(task)

    # 为了防止 task 被永远保持强引用,而无法被垃圾回收
    # 让每个 task 在结束后将自己从集合中移除:
    task.add_done_callback(background_tasks.discard)
复制成功

自此,我们保护了 Task 免遭垃圾回收的毒手,移除了一个偶发的、迷惑的 bug 隐患。


引用:

  1. asyncio: Use strong references for free-flying tasks · Issue #91887 · python/cpython (github.com) https://github.com/python/cpython/issues/91887

  2. Incorrect `Context` in corotine's `except` and `finally` blocks · Issue #93740 · python/cpython (github.com) https://github.com/python/cpython/issues/93740#issuecomment-1155085321

  3. 协程与任务 — Python 3.10.5 文档 https://docs.python.org/zh-cn/3/library/asyncio-task.html#asyncio.create_task

  4. fix: prevent undone task be killed by gc by ProgramRipper · Pull Request #48 · GraiaProject/BroadcastControl (github.com) https://github.com/GraiaProject/BroadcastControl/pull/48