Python asyncio.create_task() - 真的需要保留引用吗?

And*_*ndy 17 python task python-3.x python-asyncio

的文档asyncio.create_task()指出以下警告:

重要提示:保存对此函数结果的引用,以避免任务在执行过程中消失。(来源)

我的问题是:这是真的吗?

我有几个 IO 绑定的“即发即忘”任务,我想asyncio通过使用将它们提交到事件循环来同时运行这些任务asyncio.create_task()。但是,我并不真正关心协程的返回值,或者即使它们运行成功,只关心它们最终运行。一种用例是将“昂贵”计算中的数据写回 Redis 数据库。如果 Redis 可用,那就太好了。如果没有,哦,好吧,没有坏处。这就是为什么我不想/不需要await这些任务。

这是一个通用示例:

import asyncio

async def fire_and_forget_coro():
    """Some random coroutine waiting for IO to complete."""
    print('in fire_and_forget_coro()')
    await asyncio.sleep(1.0)
    print('fire_and_forget_coro() done')


async def async_main():
    """Main entry point of asyncio application."""
    print('in async_main()')
    n = 3
    for _ in range(n):
        # create_task() does not block, returns immediately.
        # Note: We do NOT save a reference to the submitted task here!
        asyncio.create_task(fire_and_forget_coro(), name='fire_and_forget_coro')

    print('awaiting sleep in async_main()')
    await asyncio.sleep(2.0) # <-- note this line
    print('sleeping done in async_main()')

    print('async_main() done.')

    # all references of tasks we *might* have go out of scope when returning from this coroutine!
    return

if __name__ == '__main__':
    asyncio.run(async_main())
Run Code Online (Sandbox Code Playgroud)

输出:

in async_main()
awaiting sleep in async_main()
in fire_and_forget_coro()
in fire_and_forget_coro()
in fire_and_forget_coro()
fire_and_forget_coro() done
fire_and_forget_coro() done
fire_and_forget_coro() done
sleeping done in async_main()
async_main() done.
Run Code Online (Sandbox Code Playgroud)

当注释掉该await asyncio.sleep()行时,我们永远看不到fire_and_forget_coro()完成。这是可以预料的:当事件循环以asyncio.run()close 开始时,任务将不再执行。但看来只要事件循环仍在运行,所有任务都会被处理,即使我从未明确创建对它们的引用。这对我来说似乎很合乎逻辑,因为事件循环本身必须引用所有计划任务才能运行它们。我们甚至可以让它们全部使用asyncio.all_tasks()

因此,我认为只要提交到的事件循环仍在运行,我就可以相信 Python 对每个计划任务至少有一个强引用,因此我不必自己管理引用。但我想在这里得到第二个意见。我是对的还是有我还没有认识到的陷阱?

如果我是对的,为什么文档中会有明确的警告?如果你不保留对某个东西的引用,它就会被垃圾回收,这是 Python 的一个常见现象。是否存在没有正在运行的事件循环但仍有一些任务对象可供引用的情况?也许在手动创建事件循环时(从未这样做过)?

And*_*ndy 15

github 上的 cpython bug tracker 有一个关于我刚刚发现的主题的未解决问题: https ://github.com/python/cpython/issues/88831

引用:

asyncio 只会保留对活动任务的弱引用(在 中_all_tasks)。如果用户没有保留对任务的引用并且该任务当前没有执行或休眠,则用户可能会收到“任务已被销毁,但它正在挂起!”。

不幸的是,我的问题的答案是肯定的。人们必须保留对计划任务的引用。

然而,github问题还描述了一个相对简单的解决方法:将所有正在运行的任务保留在a中,set()并向任务添加回调,以将其自身从a中删除set()

running_tasks = set()
# [...]
task = asyncio.create_task(some_background_function())
running_tasks.add(task)
task.add_done_callback(lambda t: running_tasks.remove(t))
Run Code Online (Sandbox Code Playgroud)

  • @AndriiMaletskyi:你是对的,Python 可以垃圾收集正在运行的任务这一事实在实践中从未在我面前爆发过。我认为这是因为我的大多数“异步即发即弃任务”都是短暂的,当垃圾收集器开始清理它们时就会完成。然而,我是“防御性编程”的忠实粉丝:如果文档没有明确说明,永远不要假设某些东西会按照您的预期工作,并且错误的假设可能会在最不方便的时候在您面前爆炸。:-) 这就是我首先开始研究他的主题的原因。 (4认同)
  • 我已经阅读了这个 gh 问题,你的 [pull request](https://github.com/python/cpython/pull/93258),[关于 hackernews 的讨论](https://news.ycombinator.com/item ?id=34754276),我仍然无法理解这一点。对我来说,你在问题中的推理很有意义,但原始 PR 中的文档和讨论却没有意义。仍然没有如何重现该示例。现在,我相信您曾经是正确的,在文档中撰写原始声明的人只是高估了问题的严重性 (2认同)
  • 只是最后一行的一个小样式改进:`task.add_done_callback(running_tasks.remove)` (2认同)