统一的异步迭代器会如何处理?

时间:2018-11-08 14:48:27

标签: python iterator python-asyncio async-iterator

说我有以下功能

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

之后的异步迭代器发生了什么

await f1()

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

1 个答案:

答案 0 :(得分:1)

  

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

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

这里的“清理”是指运行finally周围的yield__aexit__语句中使用的上下文管理器的with部分指定的代码yield周围。例如,此简单生成器中的printaiohttp.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 registers这个asyncgen终结器钩子,但是它永远都没有执行的机会,因为我们正在使用run_until_complete,它在执行协程后立即停止了循环。

如果我们尝试旋转同一事件循环更多,我们将看到执行清除:

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

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