@@ -18,15 +18,16 @@ such as a GUI toolkit's main loop (Tkinter, Qt, GTK, etc.). Instead of
1818replacing 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
3132Typical 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
77131The following three methods on :class: `BaseEventLoop ` are used internally by
0 commit comments