-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
add async generators section to asyncio internal docs #135674
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
base: main
Are you sure you want to change the base?
Changes from all commits
421d785
ba1a1a0
039ccf6
6336444
a42c487
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,12 @@ asyncio | |
======= | ||
|
||
|
||
This document describes the working and implementation details of C | ||
implementation of the | ||
This document describes the working and implementation details of the | ||
[`asyncio`](https://docs.python.org/3/library/asyncio.html) module. | ||
|
||
**This section describes the implementation details of the C implementation**. | ||
|
||
# Task management | ||
|
||
## Pre-Python 3.14 implementation | ||
|
||
|
@@ -158,7 +160,8 @@ flowchart TD | |
subgraph two["Thread deallocating"] | ||
A1{"thread's task list empty? <br> llist_empty(tstate->asyncio_tasks_head)"} | ||
A1 --> |true| B1["deallocate thread<br>free_threadstate(tstate)"] | ||
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,tstate->asyncio_tasks_head)"] | ||
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head, | ||
&tstate->asyncio_tasks_head)"] | ||
C1 --> B1 | ||
end | ||
|
||
|
@@ -205,6 +208,121 @@ In free-threading, it avoids contention on a global dictionary as | |
threads can access the current task of thier running loop without any | ||
locking. | ||
|
||
--- | ||
|
||
**This section describes the implementation details of the Python implementation**. | ||
|
||
# async generators | ||
|
||
This section describes the implementation details of async generators in `asyncio`. | ||
|
||
Since async generators are meant to be used from coroutines, | ||
their finalization (execution of finally blocks) needs | ||
to be done while the loop is running. | ||
Most async generators are closed automatically | ||
when they are fully iterated over and exhausted; however, | ||
if the async generator is not fully iterated over, | ||
it may not be closed properly, leading to the `finally` blocks not being executed. | ||
|
||
Consider the following code: | ||
```py | ||
import asyncio | ||
|
||
async def agen(): | ||
try: | ||
yield 1 | ||
finally: | ||
await asyncio.sleep(1) | ||
print("finally executed") | ||
|
||
|
||
async def main(): | ||
async for i in agen(): | ||
break | ||
|
||
loop = asyncio.EventLoop() | ||
loop.run_until_complete(main()) | ||
``` | ||
|
||
The above code will not print "finally executed", because the | ||
async generator `agen` is not fully iterated over | ||
and it is not closed manually by awaiting `agen.aclose()`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can do something like this to close it: async def main():
g = agen()
async for i in g:
break
await g.aclose() |
||
|
||
To solve this, `asyncio` uses the `sys.set_asyncgen_hooks` function to | ||
set hooks for finalizing async generators as described in | ||
[PEP 525](https://peps.python.org/pep-0525/). | ||
|
||
- **firstiter hook**: When the async generator is iterated over for the first time, | ||
the *firstiter hook* is called. The async generator is added to `loop._asyncgens` WeakSet | ||
and the event loop tracks all active async generators. | ||
|
||
- **finalizer hook**: When the async generator is about to be finalized, | ||
the *finalizer hook* is called. The event loop removes the async generator | ||
from `loop._asyncgens` WeakSet, and schedules the finalization of the async | ||
generator by creating a task calling `agen.aclose()`. This ensures that the | ||
finally block is executed while the event loop is running. When the loop is | ||
shutting down, the loop checks if there are active async generators and if so, | ||
it similarly schedules the finalization of all active async generators by calling | ||
`agen.aclose()` on each of them and waits for them to complete before shutting | ||
down the loop. | ||
|
||
This ensures that the async generator's `finally` blocks are executed even | ||
if the generator is not explicitly closed. | ||
|
||
Consider the following example: | ||
|
||
```python | ||
import asyncio | ||
|
||
async def agen(): | ||
try: | ||
yield 1 | ||
yield 2 | ||
finally: | ||
print("executing finally block") | ||
|
||
async def main(): | ||
async for item in agen(): | ||
print(item) | ||
break # not fully iterated | ||
|
||
asyncio.run(main()) | ||
``` | ||
|
||
```mermaid | ||
flowchart TD | ||
subgraph one["Loop running"] | ||
A["asyncio.run(main())"] --> B | ||
B["set async generator hooks <br> sys.set_asyncgen_hooks()"] --> C | ||
C["async for item in agen"] --> F | ||
F{"first iteration?"} --> |true|D | ||
F{"first iteration?"} --> |false|H | ||
D["calls firstiter hook<br>loop._asyncgen_firstiter_hook(agen)"] --> E | ||
E["add agen to WeakSet<br> loop._asyncgens.add(agen)"] --> H | ||
H["item = await agen.\_\_anext\_\_()"] --> J | ||
J{"StopAsyncIteration?"} --> |true|M | ||
J{"StopAsyncIteration?"} --> |false|I | ||
I["print(item)"] --> S | ||
S{"continue iterating?"} --> |true|C | ||
S{"continue iterating?"} --> |false|M | ||
M{"agen is no longer referenced?"} --> |true|N | ||
M{"agen is no longer referenced?"} --> |false|two | ||
N["finalize agen<br>_PyGen_Finalize(agen)"] --> O | ||
O["calls finalizer hook<br>loop._asyncgen_finalizer_hook(agen)"] --> P | ||
P["remove agen from WeakSet<br>loop._asyncgens.discard(agen)"] --> Q | ||
Q["schedule task to close it<br>self.create_task(agen.aclose())"] --> R | ||
R["print('executing finally block')"] --> E1 | ||
|
||
end | ||
|
||
subgraph two["Loop shutting down"] | ||
A1{"check for alive async generators?"} --> |true|B1 | ||
B1["close all async generators <br> await asyncio.gather\(*\[ag.aclose\(\) for ag in loop._asyncgens\]"] --> R | ||
A1{"check for alive async generators?"} --> |false|E1 | ||
E1["loop.close()"] | ||
end | ||
|
||
``` | ||
|
||
[^1]: https://github.com/python/cpython/issues/123089 | ||
[^2]: https://github.com/python/cpython/issues/80788 | ||
[^2]: https://github.com/python/cpython/issues/80788 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC this section only discusses the C version -- can you call that out somehow? Previously that was implied by the opening sentence above but that has changed now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a sentence before both section to clarify that.