Skip to content
5 changes: 3 additions & 2 deletions ipykernel/iostream.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@

import zmq
from jupyter_client.session import extract_header
from tornado.ioloop import IOLoop
from zmq.eventloop.zmqstream import ZMQStream

from .thread import make_selector_io_loop

# -----------------------------------------------------------------------------
# Globals
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -67,7 +68,7 @@ def __init__(self, socket, pipe=False, session=False):
self.background_socket = BackgroundSocket(self)
self._master_pid = os.getpid()
self._pipe_flag = pipe
self.io_loop = IOLoop(make_current=False)
self.io_loop = make_selector_io_loop()
if pipe:
self._setup_pipe_in()
self._local = threading.local()
Expand Down
38 changes: 0 additions & 38 deletions ipykernel/kernelapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,43 +692,6 @@ def configure_tornado_logger(self):
handler.setFormatter(formatter)
logger.addHandler(handler)

def _init_asyncio_patch(self):
"""set default asyncio policy to be compatible with tornado

Tornado 6 (at least) is not compatible with the default
asyncio implementation on Windows

Pick the older SelectorEventLoopPolicy on Windows
if the known-incompatible default policy is in use.

Support for Proactor via a background thread is available in tornado 6.1,
but it is still preferable to run the Selector in the main thread
instead of the background.

do this as early as possible to make it a low priority and overridable

ref: https://github.com/tornadoweb/tornado/issues/2608

FIXME: if/when tornado supports the defaults in asyncio without threads,
remove and bump tornado requirement for py38.
Most likely, this will mean a new Python version
where asyncio.ProactorEventLoop supports add_reader and friends.

"""
if sys.platform.startswith("win"):
import asyncio

try:
from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy
except ImportError:
pass
# not affected
else:
if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
# WindowsProactorEventLoopPolicy is not compatible with tornado 6
# fallback to the pre-3.8 default of Selector
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())

def init_pdb(self):
"""Replace pdb with IPython's version that is interruptible.

Expand All @@ -748,7 +711,6 @@ def init_pdb(self):
@catch_config_error
def initialize(self, argv=None):
"""Initialize the application."""
self._init_asyncio_patch()
super().initialize(argv)
if self.subapp is not None:
return
Expand Down
26 changes: 25 additions & 1 deletion ipykernel/thread.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Base class for threads."""

import asyncio
import sys
from threading import Thread

from tornado.ioloop import IOLoop
Expand All @@ -8,13 +10,35 @@
SHELL_CHANNEL_THREAD_NAME = "Shell channel"


def make_selector_io_loop() -> IOLoop:
"""Create a non-current tornado ``IOLoop`` for an ipykernel service thread.

ipykernel runs its service channels -- control, IOPub, the shell channel and
subshells -- on dedicated event loops in background threads. The process-wide
asyncio loop on Windows is a ``ProactorEventLoop`` (so the main user-code loop
can spawn asyncio subprocesses, see #1468/#1469), and Proactor has no native
``add_reader``. Tornado therefore drives a Proactor loop's zmq sockets through
a helper "Tornado selector" thread. When a debugger suspends every thread at a
breakpoint on Python >= 3.12 (``sys.monitoring``), that un-exempt helper thread
freezes mid-wake and deadlocks the control/debug read path (#1469).

These service loops never need Proactor's subprocess support, so we keep them
on a ``SelectorEventLoop``: it implements ``add_reader`` natively and needs no
helper thread. Only the main/user-code loop stays on Proactor. On non-Windows
platforms the default loop is already selector-based, so this is a no-op there.
"""
if sys.platform == "win32":
return IOLoop(make_current=False, asyncio_loop=asyncio.SelectorEventLoop())
return IOLoop(make_current=False)


class BaseThread(Thread):
"""Base class for threads."""

def __init__(self, **kwargs):
"""Initialize the thread."""
super().__init__(**kwargs)
self.io_loop = IOLoop(make_current=False)
self.io_loop = make_selector_io_loop()
self.pydev_do_not_trace = True
self.is_pydev_daemon_thread = True

Expand Down
8 changes: 8 additions & 0 deletions ipykernel/zmqshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,14 @@ class ZMQInteractiveShell(InteractiveShell):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Suppress Trio's signal handling warning on Windows with ProactorEventLoop
# This occurs when Trio is imported and finds signal handling already taken by Proactor
warnings.filterwarnings(
"ignore",
message=".*Trio's signal handling code might have collided.*",
category=RuntimeWarning,
)

# tqdm has an incorrect detection of ZMQInteractiveShell when launch via
# a scheduler that bypass IPKernelApp Think of JupyterHub cluster
# spawners and co. as of end of Feb 2025, the maintainer has been
Expand Down
2 changes: 1 addition & 1 deletion tests/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_async_interrupt(asynclib, request):
__import__(asynclib)
except ImportError:
pytest.skip("Requires %s" % asynclib)
request.addfinalizer(lambda: execute("%autoawait asyncio", KC))
request.addfinalizer(lambda: execute("%autoawait " + asynclib, KC))

flush_channels(KC)
msg_id, content = execute("%autoawait " + asynclib, KC)
Expand Down
Loading