Skip to content

gh-145342: asyncio: Add guest mode for running inside external event loops#145343

Open
congzhangzh wants to merge 2 commits intopython:mainfrom
congzhangzh:add_asyncio_guest_mode
Open

gh-145342: asyncio: Add guest mode for running inside external event loops#145343
congzhangzh wants to merge 2 commits intopython:mainfrom
congzhangzh:add_asyncio_guest_mode

Conversation

@congzhangzh
Copy link

@congzhangzh congzhangzh commented Feb 28, 2026

Summary

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.

Motivation

GUI applications with a native main loop (Tkinter, Qt, GTK) cannot use
asyncio.run() without blocking or replacing the host loop. Guest mode
enables incremental migration of GUI apps to async/await without replacing
the host event loop.

Implementation

  • Add Lib/asyncio/guest.py with start_guest_run().
  • Add three public methods to BaseEventLooppoll_events(),
    process_events(), and process_ready() — that decompose _run_once()
    into independently callable steps (zero behavior change for existing code).
  • Refactor _run_once() to delegate to the three new methods.
  • Add comprehensive tests in Lib/test/test_asyncio/test_guest.py using a
    mock host loop (no GUI dependency, 12 test methods).
  • Add a Tkinter demo in Doc/includes/asyncio_guest_tkinter.py.
  • Add RST reference documentation in Doc/library/asyncio-guest.rst.
  • Add NEWS entry.

Prior Art

Inspired by Trio's start_guest_run()
and the asyncio-guest proof-of-concept.

Testing

python -m pytest Lib/test/test_asyncio/test_guest.py -v

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, and call_soon_threadsafe.

…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.
@python-cla-bot
Copy link

python-cla-bot bot commented Feb 28, 2026

All commit authors signed the Contributor License Agreement.

CLA signed

@congzhangzh
Copy link
Author

@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:)

Copy link
Contributor

@asvetlov asvetlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A deamon thread smells like a red herring

@congzhangzh
Copy link
Author

How does the proposed approach work with the Windows proactor event loop? Does the thread boundary crossing function well with IOCP ports?

It works in practice, but I agree it needs a careful check for Windows IOCP.

For the thread boundary, there is no concurrent access:

  1. The main UI thread only triggers events and runs callbacks.
  2. The backend thread completely suspends while the UI thread is active.
  3. Result: Only one thread interacts with asyncio at any given moment.

This mutually design is based on Electron: https://www.electronjs.org/blog/electron-internals-node-integration

@congzhangzh
Copy link
Author

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.

@congzhangzh
Copy link
Author

How does the proposed approach work with the Windows proactor event loop? Does the thread boundary crossing function well with IOCP ports?

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."

def _run_once(self):
asyncio design just depend on the base High level _run_once?

event_list = self._selector.select(timeout)
_run_once just depend on _select.select which is transparent for different implementation like select or IOCP

def select(self, timeout=None):
Iocp select which depend on it's internal _poll

def _poll(self, timeout=None):
_poll which depend on I/O completion ports

# 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
        # ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants