-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
Finalization of non-exhausted asynchronous generators is deferred #88684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
In the following example: def gen():
try:
yield 1
finally:
print('finalize inner')
def func():
try:
for x in gen():
break
finally:
print('finalize outer')
func()
print('END') the output in CPython is: finalize inner But in similar example for asynchronous generator: async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async for x in gen():
break
finally:
print('finalize outer')
import asyncio
asyncio.run(func())
print('END') the output in CPython is: finalize outer There is a strong link somewhere which prevents finalization of the asynchronous generator object until leaving the outer function. Tested on CPython 3.7-3.11. In PyPy "finalize inner" is not printed at all. Using closing() and aclosing() is the right way to get deterministic finalization, but in any case it would be better to get rid of strong link which prevents finalization of the asynchronous generator object. |
Can you repro this without asyncio? |
Sorry, I do not understand what do you mean. The problem is that non-exhausted asynchronous generators are not finalized until asyncio.run() is finished. This differs from synchronous generators which are finalized as early as possible (in CPython). |
I am wondering whether the issue is in asyncio.run, or in the basic async generator implementation. If the latter, you should be able to demonstrate this with a manual "driver" (trampoline?) instead of asyncio.run. |
Inlined asyncio.run(func()) is roughly equivalent to the following code: loop = asyncio.new_event_loop()
loop.run_until_complete(func())
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close() If comment out loop.run_until_complete(loop.shutdown_asyncgens()) I get the following output: finalize outer No "finalize inner" but a warning about pending task instead. On other hand, the following code for _ in func().__await__(): pass produces the output in expected order: finalize inner |
Okay, that suggests that the extra reference is being held by asyncio, right? I suppose it's in the hands of the asyncio devs then. |
The extra reference is required so that the event loop can finalize the generator in the correct context. The issue is that once the generator is about to be finalized, the event loop creates a new task to finalize it in the context but since there are no awaits after iterating over the generator the task is pending. The following script finalizes it properly as it add a sleep to switch the tasks and run the finalizer. async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async for x in gen():
break
await asyncio.sleep(0.1) # Switch task and run the finalizer
finally:
print('finalize outer')
import asyncio
asyncio.run(func())
print('END') Output: finalize inner
finalize outer
END |
The following example demonstrates the number of context switches required to run the finalizer: async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async for x in gen():
break
# Schedules callback to create a task on next loop iteration
finally:
await asyncio.sleep(0) # Creates a task to finalize gen
await asyncio.sleep(0) # Run the task to finalize gen
print('finalize outer')
import asyncio
asyncio.run(func())
print('END') Output: finalize inner
finalize outer
END |
Hopefully @serhiy-storchaka can do something with your research! |
This would probably require something like https://peps.python.org/pep-0533/ |
async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async for x in gen():
break
finally:
print('finalize outer')
import anyio
anyio.run(func, backend="trio")
print('END') this happens with trio too:
|
running it with plain coroutines results in: import threading
async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async for x in gen():
break
finally:
print('finalize outer')
import threading
coro = func()
coro.send(None) finalize inner
finalize outer
Traceback (most recent call last):
File "/home/graingert/projects/cpython/demo.py", line 19, in <module>
coro.send(None)
StopIteration |
That's a rather literalist interpretation of my question. :-) I meant whether you can come up with a repro without an event loop framework, since that would presumably make reasoning about the root cause of the problem easier. |
trio documents a one-tick finalization window here: https://github.com/python-trio/trio/blob/35319b3d4b881032416bfd733e0ac5239ce4c46d/trio/_core/_asyncgens.py#L26-L30 and here https://trio.readthedocs.io/en/stable/reference-core.html#finalization |
Ah, sorry. The whole point of the bug seems to be that the event loop hangs on to an extra reference, and there is a reason. I should really recuse myself from this issue since I don't understand async generators. |
This looks like it could be an asyncio bug - asyncio should use the firstiter hook to keep a reference to generators that need shutting down |
seems that got fixed: import threading
import trio
import asyncio
async def gen(runner):
try:
yield 1
finally:
print(f'{runner=} finalize inner')
async def func(runner):
try:
async for x in gen(runner):
break
finally:
print(f'{runner=} finalize outer')
print(f"{trio.run(func, 'trio')=}")
print(f"{asyncio.run(func('asyncio'))=}")
print(f"{func('manual').send(None)=}") # on pypy3 this doesn't print finalize inner prints:
|
The behaviour cannot be changed without significant changes to the interpreter. The documentation will be handled for this as part of #100108 so closing. |
For anyone who comes here for a possible fix which always works: import contextlib
async def gen():
try:
yield 1
finally:
print('finalize inner')
async def func():
try:
async with contextlib.aclosing(gen()) as g:
async for x in g:
break
finally:
print('finalize outer')
import asyncio
asyncio.run(func())
print('END') |
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: