@@ -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
205206threads can access the current task of thier running loop without any
206207locking.
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