Стоит ли беспокоиться об уборке или генератор будет каким-то образом собирать мусор, когда он исчезает из виду?
TL; DR Python gc и asyncio обеспечит возможную очистку не полностью повторных асинхронных генераторов.
«Очистка» здесь означает выполнение кода, указанного finally
вокруг yield
или частью __aexit__
диспетчера контекста, используемой в операторе with
вокруг yield
. Например, 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
, потому что это когда работающий генератор больше не упоминается), это возможно, потому что 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_complete
, который останавливает цикл сразу после выполнения сопрограммы.
Если бы мы попытались еще раз запустить тот же цикл обработки событий, мы бы увидели, что очистка выполнена:
>>> asyncio.get_event_loop().run_until_complete(asyncio.sleep(0))
cleaned up
В обычном асинхронном приложении это не приводит к проблемам, поскольку цикл обработки событий обычно выполняется столько же времени, сколько приложение. Если нет цикла событий для очистки асинхронных генераторов, это, вероятно, означает, что процесс все равно завершается.