联合异步迭代器会发生什么?
what happens to uniterated async iterators?
假设我有以下功能
async def f1():
async for item in asynciterator():
return
异步迭代器在
之后发生了什么
await f1()
?我应该担心清理还是发电机在看不见时会以某种方式被垃圾收集?
Should I worry about cleaning up or will the generator be somehow garbage collected when it goes out of sight?
TL;DR Python 的 gc 和 asyncio 将确保最终清理未完全迭代的异步生成器。
这里的"Cleanup"是指运行在yield
周围的finally
或者上下文管理器的__aexit__
部分指定的代码在 yield
周围的 with
语句中使用。例如,这个简单生成器中的 print
由 aiohttp.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
退出 之后仍然是 ,因为那是 运行ning 发电机不再被提及),这是可能的,因为 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
在普通的 asyncio 应用程序中,这不会导致问题,因为事件循环通常 运行s 与应用程序一样长。如果没有事件循环来清理异步生成器,则可能意味着进程正在退出。
假设我有以下功能
async def f1():
async for item in asynciterator():
return
异步迭代器在
之后发生了什么await f1()
?我应该担心清理还是发电机在看不见时会以某种方式被垃圾收集?
Should I worry about cleaning up or will the generator be somehow garbage collected when it goes out of sight?
TL;DR Python 的 gc 和 asyncio 将确保最终清理未完全迭代的异步生成器。
这里的"Cleanup"是指运行在yield
周围的finally
或者上下文管理器的__aexit__
部分指定的代码在 yield
周围的 with
语句中使用。例如,这个简单生成器中的 print
由 aiohttp.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
退出 之后仍然是 ,因为那是 运行ning 发电机不再被提及),这是可能的,因为 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
在普通的 asyncio 应用程序中,这不会导致问题,因为事件循环通常 运行s 与应用程序一样长。如果没有事件循环来清理异步生成器,则可能意味着进程正在退出。