Skip to content

Commit 970e701

Browse files
congzhangzhclaude
andcommitted
gh-145342: Update asyncio guest mode docs and NEWS
Document the non-daemon I/O thread, lifecycle and cleanup semantics, signal-handling delegation to the host, and the host requirements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent befb594 commit 970e701

3 files changed

Lines changed: 77 additions & 19 deletions

File tree

Doc/includes/asyncio_guest_tkinter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def count(progress, root):
6464
progress.configure(maximum=MAX_COUNT)
6565

6666
task = asyncio.current_task()
67-
loop = asyncio.get_event_loop()
67+
loop = asyncio.get_running_loop()
6868

6969
# Wire the Cancel button and window close to task.cancel().
7070
# Use call_soon_threadsafe so the I/O thread's selector is woken.

Doc/library/asyncio-guest.rst

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ such as a GUI toolkit's main loop (Tkinter, Qt, GTK, etc.). Instead of
1818
replacing the host loop, asyncio piggybacks on it:
1919

2020
* The **host thread** keeps running its own main loop as usual.
21-
* A **background daemon thread** blocks on the selector (I/O polling).
21+
* A **background I/O thread** blocks on the selector (I/O polling).
2222
When I/O events arrive it hands them back to the host thread via a
23-
thread-safe callback.
23+
thread-safe callback. The thread is not a daemon thread; it is joined
24+
when the guest run finishes.
2425
* The host thread then runs :meth:`~asyncio.BaseEventLoop.process_events`
2526
and :meth:`~asyncio.BaseEventLoop.process_ready` to advance the asyncio
26-
event loop by one step, then signals the background thread to poll again.
27+
event loop by one step, then signals the I/O thread to poll again.
2728

28-
This dual-thread architecture means neither the host loop nor the asyncio
29-
loop starves the other.
29+
Exactly one of the two threads touches the event loop at any moment, so
30+
neither the host loop nor the asyncio loop starves the other.
3031

3132
Typical use cases:
3233

@@ -47,23 +48,26 @@ example that embeds asyncio inside ``tkinter.mainloop()`` using
4748

4849
Run *async_fn* as a guest inside another event loop.
4950

50-
The host event loop (e.g. ``tkinter.mainloop()``) remains in control of the
51-
main thread. asyncio I/O polling runs in a daemon background thread and
52-
dispatches work back to the host thread via *run_sync_soon_threadsafe*.
51+
Must be called from the host event loop's thread. The host loop
52+
(e.g. ``tkinter.mainloop()``) remains in control of that thread;
53+
asyncio I/O polling runs in a background non-daemon thread that is
54+
joined when the run finishes.
5355

5456
:param async_fn: The async function to run as the top-level coroutine.
5557
:param args: Positional arguments forwarded to *async_fn*.
5658
:param run_sync_soon_threadsafe: A callable that schedules a zero-argument
57-
callable on the host event loop's thread in a thread-safe manner.
58-
For Tkinter use ``widget.after(0, fn)``; for Qt use a
59-
``QMetaObject.invokeMethod`` wrapper; etc.
60-
:param done_callback: Called on the host thread when *async_fn* finishes.
61-
Receives the completed :class:`Task` as its sole argument. Inspect
62-
the outcome with :meth:`Task.result`, :meth:`Task.exception`, or
59+
callable on the host event loop's thread. It must be thread-safe,
60+
must not block, and must not raise; it need not preserve ordering.
61+
For Tkinter use a ``root.call('after', 'idle', ...)`` wrapper; for
62+
Qt use a ``QMetaObject.invokeMethod`` wrapper; etc.
63+
:param done_callback: Called on the host thread after the run has fully
64+
finished and the loop is closed (see :ref:`asyncio-guest-lifecycle`).
65+
Receives the :class:`Task` as its sole argument. Inspect the
66+
outcome with :meth:`Task.result`, :meth:`Task.exception`, or
6367
:meth:`Task.cancelled`.
6468
:returns: The :class:`Task` wrapping *async_fn*.
6569

66-
To cancel the task from the host thread, use::
70+
To cancel the task from the host, use::
6771

6872
loop.call_soon_threadsafe(task.cancel)
6973

@@ -72,6 +76,56 @@ example that embeds asyncio inside ``tkinter.mainloop()`` using
7276

7377
.. versionadded:: 3.15
7478

79+
.. _asyncio-guest-lifecycle:
80+
81+
Lifecycle and Cleanup
82+
=====================
83+
84+
For the whole guest run the guest loop is the host thread's running
85+
loop: :func:`get_running_loop` works inside guest tasks,
86+
:meth:`loop.is_running() <asyncio.loop.is_running>` returns ``True``, and
87+
starting another event loop on that thread — including a nested
88+
:func:`asyncio.run` or :meth:`loop.run_until_complete` — raises
89+
:exc:`RuntimeError`. Consequently a thread that is already running an
90+
asyncio event loop cannot start a guest run.
91+
92+
When the main task finishes, cleanup equivalent to :func:`asyncio.run`
93+
takes place on the host thread: remaining tasks are cancelled,
94+
asynchronous generators and the default executor are shut down, the I/O
95+
thread is joined, and the loop is closed. Only then is *done_callback*
96+
invoked.
97+
98+
If the interpreter exits while a guest run is unfinished, the run is
99+
abandoned: the I/O thread is woken and joined so that interpreter
100+
shutdown does not hang, pending tasks are not cancelled, and
101+
*done_callback* is not called.
102+
103+
Signal Handling
104+
===============
105+
106+
In guest mode the *host* owns signal handling:
107+
108+
* The guest loop never touches :func:`signal.set_wakeup_fd`, neither to
109+
install a file descriptor nor to reset it on close, so the host's
110+
signal wake-up pipeline stays intact.
111+
* :meth:`loop.add_signal_handler` and :meth:`loop.remove_signal_handler`
112+
raise :exc:`RuntimeError`.
113+
* To let asyncio code react to a signal, catch it in the host (with
114+
:func:`signal.signal` or the host framework's facilities) and forward
115+
it into the loop with :meth:`loop.call_soon_threadsafe`.
116+
117+
Host Requirements
118+
=================
119+
120+
* *run_sync_soon_threadsafe* must be thread-safe, non-blocking, and must
121+
not raise. It may run callbacks in any order.
122+
* Host code running *outside* guest callbacks (for example a GUI button
123+
handler) must interact with the loop exclusively through
124+
:meth:`loop.call_soon_threadsafe`, even though it runs on the loop's
125+
own thread: the I/O thread may be inside the selector, and only
126+
``call_soon_threadsafe`` wakes it safely.
127+
* :meth:`loop.stop` is not supported in guest mode.
128+
75129
.. rubric:: Low-level Event Loop Methods
76130

77131
The following three methods on :class:`BaseEventLoop` are used internally by
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
Add :func:`asyncio.start_guest_run` to allow asyncio to run cooperatively
22
inside a host event loop (e.g. Tkinter, Qt, GTK). The host loop retains
3-
control of the main thread while asyncio I/O polling runs in a background
4-
daemon thread. Also adds three low-level :class:`~asyncio.BaseEventLoop`
5-
methods -- :meth:`~asyncio.BaseEventLoop.poll_events`,
3+
control of its thread while asyncio I/O polling runs in a background
4+
non-daemon thread that is joined when the run finishes. In guest mode the
5+
host owns signal handling: the loop never touches
6+
:func:`signal.set_wakeup_fd` and :meth:`loop.add_signal_handler` raises
7+
:exc:`RuntimeError`. Also adds three low-level
8+
:class:`~asyncio.BaseEventLoop` methods --
9+
:meth:`~asyncio.BaseEventLoop.poll_events`,
610
:meth:`~asyncio.BaseEventLoop.process_events`, and
711
:meth:`~asyncio.BaseEventLoop.process_ready` -- that decompose
812
:meth:`~asyncio.BaseEventLoop._run_once` into independently callable steps.

0 commit comments

Comments
 (0)