Skip to content

Commit 421d785

Browse files
add async generators section
1 parent 5f6ab92 commit 421d785

1 file changed

Lines changed: 117 additions & 3 deletions

File tree

InternalDocs/asyncio.md

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ 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
76
[`asyncio`](https://docs.python.org/3/library/asyncio.html) module.
87

8+
# Task management
99

1010
## Pre-Python 3.14 implementation
1111

@@ -158,7 +158,8 @@ flowchart TD
158158
subgraph two["Thread deallocating"]
159159
A1{"thread's task list empty? <br> llist_empty(tstate->asyncio_tasks_head)"}
160160
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)"]
161+
A1 --> |false| C1["add tasks to interpreter's task list<br> llist_concat(&tstate->interp->asyncio_tasks_head,
162+
&tstate->asyncio_tasks_head)"]
162163
C1 --> B1
163164
end
164165
@@ -205,6 +206,119 @@ In free-threading, it avoids contention on a global dictionary as
205206
threads can access the current task of thier running loop without any
206207
locking.
207208

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

209323
[^1]: https://github.com/python/cpython/issues/123089
210324
[^2]: https://github.com/python/cpython/issues/80788

0 commit comments

Comments
 (0)