Async-kernel (notebook)¶
Kernel¶
The kernel provides concurrent message handling for a more responsive kernel and better user experience.
How it works¶
Each msg_type has its own message handler function. As messages arrive the kernel schedules handling of the message as a job with a handler using a Caller. All message types are queued for execution using Caller.queue_call (a queue which runs the handler with each job in the order in which they are scheduled). The message type execute_request also permits concurrent cell execution as a task or thread.
There are a few special message types that are handle by the shell caller, including:
execute_requestcomm_msg
which are best handled directly in the shell's thread because:
- These messages were initiated by the user or affect the users code directly.
- signal based interrupts can only interrupt on the
MainThread. - It is safer for comm based widget callbacks to surface in the
MainThread.
Most other message types are handled in the 'Control' caller (thread), which means the kernel is responsive, even when the shell's thread is busy.
There are a few other message types which run in a different caller (named 'language_server') including: inspect_request, complete_request and is_complete_request (see: kernel._default_handle_in_thread).
Interface¶
The interface is a singleton which provides configuration for all classes that subclass from HasInterface.
The interface coordinates the startup and shutdown of the kernel and connections to communicate with the
kernel.
In CPython the interface can be started from the command line with a connection file. The interface is configurable via settings defined in the kernel spec, or by using the traitlets style configuration files. Multiple connections are allow on the interface.
Connections¶
Connections are provided to communicate with the kernel. There is no limit to the number of connections, however increasing the number of connections will slow
down the interface because all iopub messages are sent to every connection. Additional connection subclasses can created by subclassing Connection.
ZMQ Socket Based connection¶
Currently zmq sockets are the only external connection implemented. A ZMQConnection is automatically started when the interface is started in CPython. Multiple zmq clients can connect to the same zmq connection.
Subshells¶
A subshell is a new shell instance with its own user_ns (where cell code objects live) (In Jupyterlab / Jupyterlite you can start a subshell from a notebook from the right-click context menu "New subshell console for Notebook"). Execute requests have a separate handler queue per subshell.
import ipywidgets as ipw
from aiologic import Event
Execute request run mode¶
An execute_request can be modified to run as a task or thread.
There are a few ways this can be achieved:
- The top line of a code block
- As a tag
Warning
Only Jupyterlab and Jupyterlite allow concurrent cell execution. VSCode waits for each cell to complete before running the next.
Code for example¶
- This example requires ipywidgets
- Ensure you are running an async-kernel
Lets define a function that we'll reuse for the remainder of the notebook.
async def demo():
%callers
button = ipw.Button(description="Finish")
event = Event()
button.on_click(lambda _: event.set())
display(button)
await event
button.close()
return "Finished"
Lets run it normally (queue)
await demo()
Caller ID Parent Backend Name Children Protected
──────────────────────────────────────────────────────────────────────────────────────
current → 140060908318848 asyncio MainThread 1 🔐
140060863182528 140060908318848 asyncio Control 🔐
Button(description='Finish', style=ButtonStyle())
Run mode: task¶
task mode instructs the kernel to execute the code in a task separate to the queue, Both task and thread execute modes can be started when the kernel is busy executing. There is no imposed limitation on the number of tasks (or threads) that can be run concurrently.
See also the Caller example on how to call directly.
# task
# Tip: try running this cell while the previous cell is still busy (requires Jupyterlab or Jupyterlite).
await demo()
Caller ID Parent Backend Name Children Protected
──────────────────────────────────────────────────────────────────────────────────────
current → 140060908318848 asyncio MainThread 1 🔐
140060863182528 140060908318848 asyncio Control 🔐
Button(description='Finish', style=ButtonStyle())
Run mode: thread¶
# This time we'll use the tag to run the cell in a worker thread
await demo()
Caller ID Parent Backend Name Children Protected
──────────────────────────────────────────────────────────────────────────────────────
current → 140060908318848 asyncio MainThread 1 🔐
140060863182528 140060908318848 asyncio Control 🔐
Button(description='Finish', style=ButtonStyle())
# thread
%callers # magic provided by async-kernel
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
140060908318848 asyncio MainThread 2 🔐
140060863182528 140060908318848 asyncio Control 🔐
current → 140060611569344 140060908318848 asyncio async-kernel worker
We can also specify CallerCreateOptions as part of the top line
# thread name="My thread"
%callers
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
140060908318848 asyncio MainThread 3 🔐
140060863182528 140060908318848 asyncio Control 🔐
140060611569344 140060908318848 asyncio async-kernel worker
current → 140060594788032 140060908318848 asyncio My thread
Asynchronous magic¶
Asynchronous line (%) and cell (%%) magic functions are supported. Any line or cell magic that returns an awaitable is awaited before proceeding.
thread magic¶
This will run the code in a thread. When no settings are provided a cell worker thread is used.
Comparing thread magic with thread run mode¶
- thread magic (
%%thread) is an asynchronous magic that executes the associated code in a separate thread. - thread run mode (
# thread) instructs the kernel to run the entire cell in a separate thread, bypassing the shell execute request queue.
# Run the magic 'callers' in a caller worker thread.
%thread %callers
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
140060908318848 asyncio MainThread 3 🔐
140060863182528 140060908318848 asyncio Control 🔐
current → 140060611569344 140060908318848 asyncio async-kernel worker
140060594788032 140060908318848 asyncio My thread
To specify a thread (caller) by name
%%thread name="My executor"
%callers
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
140060908318848 asyncio MainThread 4 🔐
140060863182528 140060908318848 asyncio Control 🔐
140060611569344 140060908318848 asyncio async-kernel worker
140060594788032 140060908318848 asyncio My thread
current → 140060292806336 140060908318848 asyncio My executor
Many of arguments accepted on Caller.get are also supported. Let's use a thread with a trio backend.
Note: trio must be installed.
%%thread name="My trio executor" backend=trio
%callers
import trio
await trio.sleep(0)
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
140060908318848 asyncio MainThread 5 🔐
140060863182528 140060908318848 asyncio Control 🔐
140060611569344 140060908318848 asyncio async-kernel worker
140060594788032 140060908318848 asyncio My thread
140060292806336 140060908318848 asyncio My executor
current → 140060276025024 140060908318848 trio My trio executor
Specify the backend¶
Code that is written for a specific backend (asyncio or trio) can be run in the same thread with one of the following:
Line magic - The code following the magic on the same line is run using the specified backend.
%trio%asyncio
Cell magic - The code block is run using the specified backend.
%%trio%asyncio
import asyncio # noqa: F401 # pyright: ignore[reportUnusedImport]
%asyncio await asyncio.sleep(0) # This code gets run in an asyncio task
%trio await trio.sleep(0) # trio run as line magic
%%trio # trio cell magic
def print_info():
from aiologic.lowlevel import current_async_library
print(f"""
Kernel backend: {get_ipython().kernel.parent.backend}
Current backend: { current_async_library()}
""")
print_info()
await trio.sleep(0)
%callers
%asyncio print_info()
%asyncio %callers
await trio.sleep(0)
Kernel backend: asyncio
Current backend: trio
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
current → 140060908318848 asyncio MainThread 5 🔐
140060863182528 140060908318848 asyncio Control 🔐
140060611569344 140060908318848 asyncio async-kernel worker
140060594788032 140060908318848 asyncio My thread
140060292806336 140060908318848 asyncio My executor
140060276025024 140060908318848 trio My trio executor
Kernel backend: asyncio
Current backend: asyncio
Caller ID Parent Backend Name Children Protected
───────────────────────────────────────────────────────────────────────────────────────────────
current → 140060908318848 asyncio MainThread 5 🔐
140060863182528 140060908318848 asyncio Control 🔐
140060611569344 140060908318848 asyncio async-kernel worker
140060594788032 140060908318848 asyncio My thread
140060292806336 140060908318848 asyncio My executor
140060276025024 140060908318848 trio My trio executor