首页 > 解决方案 > 联合异步迭代器会发生什么?

问题描述

说我有以下功能

async def f1():
    async for item in asynciterator():
        return

之后异步迭代器会发生什么

await f1()

? 我应该担心清理还是发电机在看不见时会以某种方式被垃圾收集?

标签: pythoniteratorpython-asyncioasync-iterator

解决方案


我应该担心清理还是发电机在看不见时会以某种方式被垃圾收集?

TL;DR Python 的 gc 和 asyncio 将确保最终清理不完全迭代的异步生成器。

这里的“清理”是指运行由 a 指定的代码finallyyield__aexit__在. 例如,在这个简单的生成器中,调用 a 的机制与关闭其资源的机制相同:withyieldprintaiohttp.ClientSession

async def my_gen():
    try:
        yield 1
        yield 2
        yield 3
    finally:
        await asyncio.sleep(0.1)  # make it interesting by awaiting
        print('cleaned up')

如果你运行一个遍历整个生成器的协程,清理将立即执行:

>>> async def test():
...     gen = my_gen()
...     async for _ in gen:
...         pass
...     print('test done')
... 
>>> asyncio.get_event_loop().run_until_complete(test())
cleaned up
test done

请注意在循环之后如何立即执行清理,即使生成器仍在范围内而没有机会收集垃圾。这是因为async for循环确保异步生成器在循环耗尽时进行清理。

问题是当循环没有耗尽时会发生什么:

>>> async def test():
...     gen = my_gen()
...     async for _ in gen:
...         break  # exit at once
...     print('test done')
... 
>>> asyncio.get_event_loop().run_until_complete(test())
test done

这里gen超出了范围,但根本没有进行清理。如果您使用普通生成器尝试此操作,清理将由立即计数的引用调用(尽管仍然退出之后test,因为那是不再引用正在运行的生成器的时候),这是可能的,因为gen不参与循环:

>>> def my_gen():
...     try:
...         yield 1
...         yield 2
...         yield 3
...     finally:
...         print('cleaned up')
... 
>>> def test():
...     gen = my_gen()
...     for _ in gen:
...         break
...     print('test done')
... 
>>> test()
test done
cleaned up

作为my_gen一个异步生成器,它的清理也是异步的。这意味着它不能只由垃圾收集器执行,它需要由事件循环运行。为了实现这一点,asyncio注册了 asyncgen 终结器钩子,但它永远不会有机会执行,因为我们使用run_until_completewhich 在执行协程后立即停止循环。

如果我们尝试再次旋转相同的事件循环,我们会看到执行了清理:

>>> asyncio.get_event_loop().run_until_complete(asyncio.sleep(0))
cleaned up

在普通的 asyncio 应用程序中,这不会导致问题,因为事件循环通常与应用程序一样长时间运行。如果没有事件循环来清理异步生成器,则可能意味着该进程无论如何都在退出。


推荐阅读