Skip to content

Commit 0d9d489

Browse files
add async generators section to asyncio internal docs (#135674)
1 parent 621a8bd commit 0d9d489

File tree

1 file changed

+122
-4
lines changed

1 file changed

+122
-4
lines changed

InternalDocs/asyncio.md

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ asyncio
22
=======
33

44

5-
This document describes the working and implementation details of C
6-
implementation of the
5+
This document describes the working and implementation details of the
76
[`asyncio`](https://docs.python.org/3/library/asyncio.html) module.
87

8+
**The following section describes the implementation details of the C implementation**.
9+
10+
# Task management
911

1012
## Pre-Python 3.14 implementation
1113

@@ -158,7 +160,8 @@ flowchart TD
158160
subgraph two["Thread deallocating"]
159161
A1{"thread's task list empty? <br> llist_empty(tstate->asyncio_tasks_head)"}
160162
A1 --> |true| B1["deallocate thread<br>free_threadstate(tstate)"]
161-
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,tstate->asyncio_tasks_head)"]
163+
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,
164+
&tstate->asyncio_tasks_head)"]
162165
C1 --> B1
163166
end
164167
@@ -205,6 +208,121 @@ In free-threading, it avoids contention on a global dictionary as
205208
threads can access the current task of thier running loop without any
206209
locking.
207210

211+
---
212+
213+
**The following section describes the implementation details of the Python implementation**.
214+
215+
# async generators
216+
217+
This section describes the implementation details of async generators in `asyncio`.
218+
219+
Since async generators are meant to be used from coroutines,
220+
their finalization (execution of finally blocks) needs
221+
to be done while the loop is running.
222+
Most async generators are closed automatically
223+
when they are fully iterated over and exhausted; however,
224+
if the async generator is not fully iterated over,
225+
it may not be closed properly, leading to the `finally` blocks not being executed.
226+
227+
Consider the following code:
228+
```py
229+
import asyncio
230+
231+
async def agen():
232+
try:
233+
yield 1
234+
finally:
235+
await asyncio.sleep(1)
236+
print("finally executed")
237+
238+
239+
async def main():
240+
async for i in agen():
241+
break
242+
243+
loop = asyncio.EventLoop()
244+
loop.run_until_complete(main())
245+
```
246+
247+
The above code will not print "finally executed", because the
248+
async generator `agen` is not fully iterated over
249+
and it is not closed manually by awaiting `agen.aclose()`.
250+
251+
To solve this, `asyncio` uses the `sys.set_asyncgen_hooks` function to
252+
set hooks for finalizing async generators as described in
253+
[PEP 525](https://peps.python.org/pep-0525/).
254+
255+
- **firstiter hook**: When the async generator is iterated over for the first time,
256+
the *firstiter hook* is called. The async generator is added to `loop._asyncgens` WeakSet
257+
and the event loop tracks all active async generators.
258+
259+
- **finalizer hook**: When the async generator is about to be finalized,
260+
the *finalizer hook* is called. The event loop removes the async generator
261+
from `loop._asyncgens` WeakSet, and schedules the finalization of the async
262+
generator by creating a task calling `agen.aclose()`. This ensures that the
263+
finally block is executed while the event loop is running. When the loop is
264+
shutting down, the loop checks if there are active async generators and if so,
265+
it similarly schedules the finalization of all active async generators by calling
266+
`agen.aclose()` on each of them and waits for them to complete before shutting
267+
down the loop.
268+
269+
This ensures that the async generator's `finally` blocks are executed even
270+
if the generator is not explicitly closed.
271+
272+
Consider the following example:
273+
274+
```python
275+
import asyncio
276+
277+
async def agen():
278+
try:
279+
yield 1
280+
yield 2
281+
finally:
282+
print("executing finally block")
283+
284+
async def main():
285+
async for item in agen():
286+
print(item)
287+
break # not fully iterated
288+
289+
asyncio.run(main())
290+
```
291+
292+
```mermaid
293+
flowchart TD
294+
subgraph one["Loop running"]
295+
A["asyncio.run(main())"] --> B
296+
B["set async generator hooks <br> sys.set_asyncgen_hooks()"] --> C
297+
C["async for item in agen"] --> F
298+
F{"first iteration?"} --> |true|D
299+
F{"first iteration?"} --> |false|H
300+
D["calls firstiter hook<br>loop._asyncgen_firstiter_hook(agen)"] --> E
301+
E["add agen to WeakSet<br> loop._asyncgens.add(agen)"] --> H
302+
H["item = await agen.\_\_anext\_\_()"] --> J
303+
J{"StopAsyncIteration?"} --> |true|M
304+
J{"StopAsyncIteration?"} --> |false|I
305+
I["print(item)"] --> S
306+
S{"continue iterating?"} --> |true|C
307+
S{"continue iterating?"} --> |false|M
308+
M{"agen is no longer referenced?"} --> |true|N
309+
M{"agen is no longer referenced?"} --> |false|two
310+
N["finalize agen<br>_PyGen_Finalize(agen)"] --> O
311+
O["calls finalizer hook<br>loop._asyncgen_finalizer_hook(agen)"] --> P
312+
P["remove agen from WeakSet<br>loop._asyncgens.discard(agen)"] --> Q
313+
Q["schedule task to close it<br>self.create_task(agen.aclose())"] --> R
314+
R["print('executing finally block')"] --> E1
315+
316+
end
317+
318+
subgraph two["Loop shutting down"]
319+
A1{"check for alive async generators?"} --> |true|B1
320+
B1["close all async generators <br> await asyncio.gather\(*\[ag.aclose\(\) for ag in loop._asyncgens\]"] --> R
321+
A1{"check for alive async generators?"} --> |false|E1
322+
E1["loop.close()"]
323+
end
324+
325+
```
208326

209327
[^1]: https://github.com/python/cpython/issues/123089
210-
[^2]: https://github.com/python/cpython/issues/80788
328+
[^2]: https://github.com/python/cpython/issues/80788

0 commit comments

Comments
 (0)