{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "import asyncio\n", "\n", "pn.extension()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "User applications frequently have to do multiple tasks at the same time such as processing user input, making I/O requests or running long running calcultions. At the same time the application should continue to responsive to user input. This problem can be solved in a number of different ways and here we will look primarily at two approaches, asynchronous callbacks and concurrency using multiple threads." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Asynchronous functions\n", "\n", "Python has natively supported asynchronous functions since version 3.5, for a quick overview of some of the concepts involved see [the Python documentation](https://docs.python.org/3/library/asyncio-task.html). For full asyncio support in Panel you will have to use `python>=3.8`.\n", "\n", "One of the major benefits of leveraging async functions is that it is simple to write callbacks which will perform some longer running IO tasks in the background. Below we simulate this by creating a `Button` which will update some text when it starts and finishes running a long-running background task (here simulated using `asyncio.sleep`. If you are running this in the notebook you will note that you can start multiple tasks and it will update the text immediately but continue in the background:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "button = pn.widgets.Button(name='Click me!')\n", "text = pn.widgets.StaticText()\n", "\n", "async def run_async(event):\n", " text.value = f'Running {event.new}'\n", " await asyncio.sleep(2)\n", " text.value = f'Finished {event.new}'\n", "\n", "button.on_click(run_async)\n", "\n", "pn.Row(button, text)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that `on_click` is simple one way of registering an asynchronous callback, using `.param.watch` is also supported and so is scheduling asynchronous periodic callbacks with `pn.state.add_periodic_callback`.\n", "\n", "It is important to note that asynchronous callbacks operate without locking the underlying bokeh Document, which means Bokeh models cannot be safely modified by default. Usually this is not an issue because modifying Panel components appropriately schedules updates to underlying Bokeh models, however in cases where we want to modify a Bokeh model directly, e.g. when embedding and updating a Bokeh plot in a Panel application we explicitly have to decorate the asynchronous callback with `pn.io.with_lock`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from bokeh.plotting import figure\n", "from bokeh.models import ColumnDataSource\n", "\n", "button = pn.widgets.Button(name='Click me!')\n", "\n", "p = figure(width=500, height=300)\n", "cds = ColumnDataSource(data={'x': [0], 'y': [0]})\n", "p.line(x='x', y='y', source=cds)\n", "pane = pn.pane.Bokeh(p)\n", "\n", "@pn.io.with_lock\n", "async def stream(event):\n", " await asyncio.sleep(1)\n", " x, y = cds.data['x'][-1], cds.data['y'][-1]\n", " cds.stream({'x': list(range(x+1, x+6)), 'y': y+np.random.randn(5).cumsum()})\n", " pane.param.trigger('object')\n", " \n", "# Equivalent to `.on_click` but shown\n", "button.param.watch(stream, 'clicks')\n", "\n", "pn.Row(button, pane)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Concurrency\n", "\n", "Asynchronous processing can be very helpful for IO bound tasks, however if you have to perform actual computations it won't help you at all since those tasks will continue to block the running thread. It is also not always easy or possible to peform all IO bound tasks asynchronously. Therefore threading can be a very valuable tool in your toolbox. \n", "\n", "Below we will demonstrate an example of a Thread which we start in the background to process items we put in a queue for processing. We simulate the processing with a `time.sleep` but it could be any long-running computation. The `threading.Condition` allows us to manipulate the global shared `queue`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "import threading\n", "\n", "c = threading.Condition()\n", "\n", "button = pn.widgets.Button(name='Click to launch')\n", "text = pn.widgets.StaticText()\n", "\n", "queue = []\n", "\n", "def callback():\n", " global queue\n", " while True:\n", " c.acquire()\n", " for i, q in enumerate(queue):\n", " text.value = f'Processing item {i+1} of {len(queue)} items in queue.'\n", " time.sleep(2)\n", " queue.clear()\n", " text.value = \"Queue empty\"\n", " c.release()\n", " time.sleep(1)\n", " \n", "thread = threading.Thread(target=callback)\n", "thread.start()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will create a callback that puts new items for processing on the queue when a button is clicked:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def on_click(event):\n", " queue.append(event)\n", "\n", "button.on_click(on_click)\n", "\n", "pn.Row(button, text).servable()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since the processing happens on a separate thread the application itself can still remain responsive to further user input (such as putting new items on the queue)." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 4 }