Concurrency¶
Async kernel runs message request with one of the following RunModes
.
- blocking: Run the handler directly blocking the message loop
- queue: Run in a queue
- task: Run in a task
- thread: Run in a Caller thread
The kernel decides the run mode dynamically with the method [get_run_mode][async_kernel.kernel.Kernel.get_run_mode].
from async_kernel import utils
from async_kernel.typing import KernelConcurrencyMode, MsgType, RunMode
kernel = utils.get_kernel()
kernel.get_run_mode(MsgType.comm_msg)
<RunMode.task: 'task'>
The kernel.concurrency_mode
will also change the RunMode
that is returned by kernel.get_run_mode
.
kernel.concurrency_mode = KernelConcurrencyMode.blocking
print(f"""
kernel.concurrency_mode: {kernel.concurrency_mode!s}
kernel.get_run_mode: {kernel.get_run_mode(MsgType.comm_msg).name}""")
kernel.concurrency_mode = KernelConcurrencyMode.default
print(f"""
kernel.concurrency_mode: {kernel.concurrency_mode!s}
kernel.get_run_mode: {kernel.get_run_mode(MsgType.comm_msg).name}""")
kernel.concurrency_mode: blocking kernel.get_run_mode: blocking
kernel.concurrency_mode: default kernel.get_run_mode: task
Below is a list of the run modes for the currently available concurrency modes.
Note
`blocking` mode is roughly equivalent to how IpyKernel < 7.0 operates.
data = kernel.all_concurrency_run_modes()
try:
import pandas as pd
except ImportError:
print(data)
else:
data = pd.DataFrame(data)
data["RunMode"] = data.RunMode.str.replace("##", "")
data = data.pivot(index="MsgType", columns=["KernelConcurrencyMode", "SocketID"], values="RunMode") # noqa: PD010
display(data)
KernelConcurrencyMode | default | blocking | default | blocking |
---|---|---|---|---|
SocketID | shell | shell | control | control |
MsgType | ||||
comm_close | blocking | blocking | blocking | blocking |
comm_info_request | blocking | blocking | blocking | blocking |
comm_msg | task | blocking | task | blocking |
comm_open | blocking | blocking | blocking | blocking |
complete_request | thread | blocking | thread | blocking |
debug_request | None | blocking | task | blocking |
execute_request | queue | blocking | task | blocking |
history_request | thread | blocking | thread | blocking |
inspect_request | thread | blocking | thread | blocking |
interrupt_request | task | blocking | task | blocking |
is_complete_request | thread | blocking | thread | blocking |
kernel_info_request | blocking | blocking | blocking | blocking |
shutdown_request | None | blocking | task | blocking |
Execute request run mode¶
There are a few options to modify how code cells are run.
- Metadata
- Directly in code
- tags
- Message header (in custom messages)
Warning
Only Jupyter lab is known to allow concurrent execution of cells.
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():
import threading
import anyio
from ipywidgets import Button
from async_kernel import Caller, utils
print("Run mode:", utils.get_job()["run_mode"])
print(f"Thread name: '{threading.current_thread().name}'")
button = Button(description="Finish")
event = anyio.Event()
caller = Caller() # Use caller so the `##thread` example works.
# This is because widget messages are received by the shell in the main thread. The event is being waited in this thread.
button.on_click(lambda _: caller.call_direct(event.set))
display(button)
await event.wait()
button.close()
print(f"Finished ... thread name: '{threading.current_thread().name}'")
return "Finished"
Lets run it normally (queue)
await demo()
Run mode:
##queue
Thread name: 'MainThread'
Button(description='Finish', style=ButtonStyle())
##queue
# Tip: try running this cell while the previous cell is still busy.
await demo()
Run mode:
##queue
Thread name: 'MainThread'
Button(description='Finish', style=ButtonStyle())
Run mode: task¶
The 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.
RunMode.task # noqa: B018 # Using the literal `RunMode` values directly is also possible. Though it may show up as a [Flake8 B018 issue](https://docs.astral.sh/ruff/rules/useless-expression/)
await demo()
Run mode:
##queue
Thread name: 'MainThread'
Button(description='Finish', style=ButtonStyle())
Run mode: thread¶
# This time we'll use the tag to run the cell in a Thread
await demo()
Run mode:
##queue
Thread name: 'MainThread'
Button(description='Finish', style=ButtonStyle())
##thread
%callers # magic provided by async kernel
Running Protected Name
──────────────────────────────────────────────────────────────────────
✓ 🔐 MainThread
✓ 🔐 ControlThread
✓ Thread-3 (anyio_run_caller)
✓ Thread-4 (anyio_run_caller) ← current thread