gh-145342: asyncio: Add guest mode for running inside external event loops#145343
gh-145342: asyncio: Add guest mode for running inside external event loops#145343congzhangzh wants to merge 2 commits intopython:mainfrom
Conversation
…event loops Add asyncio.start_guest_run() which allows asyncio to run cooperatively inside a host event loop (e.g. Tkinter, Qt, GTK). The host loop stays in control of the main thread while asyncio I/O polling runs in a background daemon thread. Implementation: - Add three public methods to BaseEventLoop -- poll_events(), process_events(), and process_ready() -- that decompose _run_once() into independently callable steps. - Refactor _run_once() to delegate to these three methods (zero behaviour change for existing code). - Add Lib/asyncio/guest.py with start_guest_run(). - Add comprehensive tests using a mock host loop (no GUI dependency). - Add a Tkinter demo in Doc/includes/. Inspired by Trio start_guest_run() and the asyncio-guest project.
|
@gvanrossum Hi Guido, I try to add guest mode to asyncio now, as I found that if I do not do it now, I will never have time to do it:) |
asvetlov
left a comment
There was a problem hiding this comment.
How does the proposed approach work with the Windows proactor event loop?
Does the thread boundary crossing function well with IOCP ports?
| _process_on_host([]) | ||
|
|
||
| threading.Thread( | ||
| target=_backend, daemon=True, name='asyncio-guest-io' |
There was a problem hiding this comment.
A deamon thread smells like a red herring
It works in practice, but I agree it needs a careful check for Windows IOCP. For the thread boundary, there is no concurrent access:
This mutually design is based on Electron: https://www.electronjs.org/blog/electron-internals-node-integration |
|
BTW, the binary concept of a loop being 'running' or 'not running' breaks down a bit in guest mode. Internals like asyncio.sleep depend on it running, while other parts expect it stopped. We might need to adjust this abstraction. |
It worked well in my past tests: https://github.com/congzhangzh/webview_python/tree/main/examples/async_with_asyncio_guest_run Initially, I tried hooking directly into libuv or another event loop, but I later realized the event loop model is transparent to my solution. Rather than relying on a standalone _run_once tick, the abstraction my solution actually depends on is select. For instance, the Windows IOCP proactor just relies on its internal implementation under the hood." cpython/Lib/asyncio/base_events.py Line 1977 in 6c417e4 cpython/Lib/asyncio/base_events.py Line 2019 in 6c417e4 cpython/Lib/asyncio/windows_events.py Line 444 in 6c417e4 cpython/Lib/asyncio/windows_events.py Line 762 in 6c417e4 # windows_events.py
class IocpProactor:
"""Proactor implementation using IOCP."""
# .. #
def select(self, timeout=None):
if not self._results:
self._poll(timeout)
tmp = self._results
self._results = []
try:
return tmp
finally:
# Needed to break cycles when an exception occurs.
tmp = None
def _poll(self, timeout=None):
# ...
while True:
status = _overlapped.GetQueuedCompletionStatus(self._iocp, ms)
if status is None:
break
ms = 0
# ... |
Summary
Add
asyncio.start_guest_run()which allows asyncio to run cooperativelyinside a host event loop (e.g. Tkinter, Qt, GTK). The host loop stays in
control of the main thread while asyncio I/O polling runs in a background
daemon thread.
Motivation
GUI applications with a native main loop (Tkinter, Qt, GTK) cannot use
asyncio.run()without blocking or replacing the host loop. Guest modeenables incremental migration of GUI apps to async/await without replacing
the host event loop.
Implementation
Lib/asyncio/guest.pywithstart_guest_run().BaseEventLoop—poll_events(),process_events(), andprocess_ready()— that decompose_run_once()into independently callable steps (zero behavior change for existing code).
_run_once()to delegate to the three new methods.Lib/test/test_asyncio/test_guest.pyusing amock host loop (no GUI dependency, 12 test methods).
Doc/includes/asyncio_guest_tkinter.py.Doc/library/asyncio-guest.rst.Prior Art
Inspired by Trio's
start_guest_run()and the asyncio-guest proof-of-concept.
Testing
All 12 tests pass. The mock host loop tests cover: simple return, None return,
arguments, exceptions, cancellation from host,
asyncio.sleep(), task creation,asyncio.gather(),call_later, andcall_soon_threadsafe.