From 421d7851bd1ed4097fa4f495cfe182af4d40fb76 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 18 Jun 2025 18:23:23 +0530 Subject: [PATCH 1/5] add async generators section --- InternalDocs/asyncio.md | 120 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/InternalDocs/asyncio.md b/InternalDocs/asyncio.md index b60fe70478a6bc..fcdc6250c6d125 100644 --- a/InternalDocs/asyncio.md +++ b/InternalDocs/asyncio.md @@ -2,10 +2,10 @@ asyncio ======= -This document describes the working and implementation details of C -implementation of the +This document describes the working and implementation details [`asyncio`](https://docs.python.org/3/library/asyncio.html) module. +# Task management ## Pre-Python 3.14 implementation @@ -158,7 +158,8 @@ flowchart TD subgraph two["Thread deallocating"] A1{"thread's task list empty?
llist_empty(tstate->asyncio_tasks_head)"} A1 --> |true| B1["deallocate thread
free_threadstate(tstate)"] - A1 --> |false| C1["add tasks to interpreter's task list
llist_concat(&tstate->interp->asyncio_tasks_head,tstate->asyncio_tasks_head)"] + A1 --> |false| C1["add tasks to interpreter's task list
llist_concat(&tstate->interp->asyncio_tasks_head, + &tstate->asyncio_tasks_head)"] C1 --> B1 end @@ -205,6 +206,119 @@ In free-threading, it avoids contention on a global dictionary as threads can access the current task of thier running loop without any locking. +--- + +# async generators + +This section describes the implementation details of async generators in `asyncio`. + +Since async generators are meant to be used from coroutines, +the finalization (execution of finally blocks) of the it needs +to be done while the loop is running. +Most async generators are closed automatically when +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()`. + +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
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
loop._asyncgen_firstiter_hook(agen)"] --> E + E["add agen to WeakSet
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
_PyGen_Finalize(agen)"] --> O + O["calls finalizer hook
loop._asyncgen_finalizer_hook(agen)"] --> P + P["remove agen from WeakSet
loop._asyncgens.discard(agen)"] --> Q + Q["schedule task to close it
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
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 \ No newline at end of file From ba1a1a0f70a9a8850d577315f172092b7e446411 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 18 Jun 2025 19:48:17 +0530 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- InternalDocs/asyncio.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InternalDocs/asyncio.md b/InternalDocs/asyncio.md index fcdc6250c6d125..544fcc334cc171 100644 --- a/InternalDocs/asyncio.md +++ b/InternalDocs/asyncio.md @@ -2,7 +2,7 @@ asyncio ======= -This document describes the working and implementation details +This document describes the working and implementation details of the [`asyncio`](https://docs.python.org/3/library/asyncio.html) module. # Task management @@ -213,9 +213,9 @@ locking. This section describes the implementation details of async generators in `asyncio`. Since async generators are meant to be used from coroutines, -the finalization (execution of finally blocks) of the it needs +the finalization (execution of finally blocks) of it needs to be done while the loop is running. -Most async generators are closed automatically when +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. From 039ccf6f6af7ccbbd9946e25f9e4d34da21ce617 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 22 Jun 2025 09:14:42 +0530 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Guido van Rossum --- InternalDocs/asyncio.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InternalDocs/asyncio.md b/InternalDocs/asyncio.md index 544fcc334cc171..01a327865a2ee6 100644 --- a/InternalDocs/asyncio.md +++ b/InternalDocs/asyncio.md @@ -213,10 +213,10 @@ locking. This section describes the implementation details of async generators in `asyncio`. Since async generators are meant to be used from coroutines, -the finalization (execution of finally blocks) of it needs +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, +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. From 63364448bf6fb69d5008f29934894037fda663d3 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 22 Jun 2025 09:23:02 +0530 Subject: [PATCH 4/5] add sentence about different implementations --- InternalDocs/asyncio.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InternalDocs/asyncio.md b/InternalDocs/asyncio.md index 01a327865a2ee6..b85265d3f083f3 100644 --- a/InternalDocs/asyncio.md +++ b/InternalDocs/asyncio.md @@ -5,6 +5,8 @@ asyncio 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 @@ -208,6 +210,8 @@ locking. --- +**This section describes the implementation details of the Python implementation**. + # async generators This section describes the implementation details of async generators in `asyncio`. From a42c4871bd971a9eeaa41bf89b500c91a2b585a0 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sun, 22 Jun 2025 09:30:49 +0530 Subject: [PATCH 5/5] add newline at end --- InternalDocs/asyncio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InternalDocs/asyncio.md b/InternalDocs/asyncio.md index b85265d3f083f3..5dfe155425e8e0 100644 --- a/InternalDocs/asyncio.md +++ b/InternalDocs/asyncio.md @@ -325,4 +325,4 @@ flowchart TD ``` [^1]: https://github.com/python/cpython/issues/123089 -[^2]: https://github.com/python/cpython/issues/80788 \ No newline at end of file +[^2]: https://github.com/python/cpython/issues/80788