Async and Concurrency (for dummies)

Hey,

For those of you who use and love Panel but are no professionals like myself here’s a little help for Async and Concurrency. It’s a “bit more” straightforward than the official user guide’s description.

Please, see code snippet and link below for details. Tried and tested on Heroku.

@unsync
async def unsync_async():
   await asyncio.sleep(0.1)
   return 'I like decorators'

unsync_async().result()

Unsync

To give you an idea…
Declare a widget with placeholder info for initial load then in the background run and async function to update its value/object/etc.

1 Like

Hi,
That looks interesting.
If I understand this correctly, everything still happens in the same thread right? So there is no need for pn.io.unlocked() ? Does this keep the GUI responsive while the task is being executed?

Also, perhaps a long shot, any way of making this work with dask?
For what I’d like to do see this post here: Panel webapp with dask/bokeh/tornado and asynchronous tasks - #5 by Jhsmit

Is this equivalent to pn.state.onload(load_data) Defer data load — Panel 0.11.3 documentation

@Jhsmit @ahuang11
What I can say is that the GUI does stay responsive. I can’t confirm your other questions regarding threads/Dask/etc.

An example I can give you. A Panel app running on Heroku took about 30 seconds to load up and this was not due to dyno sleep/boot time. During the 30 seconds data got downloaded and processed from database to be turned into lists for select widgets, datepicker widgets, etc.

Once I started using the above method, the app load time dropped below 5 seconds and was responsive before the widgets’ objects/values got updated, if that makes sense.

For me the main advantage of this method is its readability. Apart from speed of course…

Also, for those of you not familiar with unsync… when you are calling your function, you don’t have to use .result()

Further to this, check out Unsync’s GitHub page to have a better understanding of how you can use the @unsync decorator with normal functions or with async functions, how you can mix them, how you can chain them. How .result() works. What these mean in terms of threading, etc.

Unsync - GitHub

…and all the above seems to be working with Panel.

1 Like

I’ll have a look, my last tangle with Dask i had issues because the dask future’s are different from asyncio features. But perhaps this library can help.

What I’m using now with the period callback seems to incur high CPU usage on client PC’s…

Hi @danmaty

Is it possible for you to share a larger example with the community of using unsync? It would be really interesting.

It could be as a showcase on Discourse, A blog post, A Gallery Notebook or an addition to the existing documentation.

Just an idea. We all need help creating high performing apps.

Hi @Marc
I can try to put something together. Happy to share.

1 Like

Hi @Marc

Have a look at the below and let me know if you think it’s too much or too little. Just a simple demo with three Unsync wrapped functions with non-blocking sleep calls manipulating panel objects.

If this is something you had in mind or not far off I can write a few sentences or something to give it some shape/form.

import panel as pn
import param
from unsync import unsync
from time import sleep as sl
from time import strftime as st
from panel.layout.gridstack import GridStack

CSS = """
#header {
    background-color: rgba(255,255,255,0.33);
    -ms-box-shadow: none !important;
    -o-box-shadow: none !important;
    -moz-box-shadow: none !important;
    -webkit-box-shadow: none !important;
    box-shadow: none !important;
}

.main-content {
    transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665);
    width: 100%;
    height: calc(100vh - 76px);
    padding: 10px;
}

body {
    background: radial-gradient(#B8BEB4, #71685F);
}
 
.bk-root button.button {
    width:65px;
    height:55px;
    position:absolute;
    bottom:0px;
    background-color: rgba(255,255,255,0.33);
    color:#FFF;
    border-radius:25px;
    text-align:center;
    box-shadow: 2px 2px 3px black;
    transition: all 0.2s ease-in-out;
    font-size:30px;
    border-color: rgba(255,255,255,0.33);
}

.bk-root button.button:hover {
    box-shadow: 4px 4px 3px black;
    background-color: rgba(255,255,255,0.33);
    transform: scale(1.05);
    border-color: rgba(255,255,255,0.33);
    cursor: pointer;
}

.bk-root button.button:active {
    transform: translateY(1px);
}
"""

pn.config.sizing_mode = 'stretch_both'
pn.extension('gridstack', raw_css=[CSS])

playSVG="""<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20"/></g><g><g><path d="M10,2c-4.42,0-8,3.58-8,8s3.58,8,8,8s8-3.58,8-8S14.42,2,10,2z M10,16.5c-3.58,0-6.5-2.92-6.5-6.5S6.42,3.5,10,3.5 s6.5,2.92,6.5,6.5S13.58,16.5,10,16.5z"/><polygon points="8,13.5 13.5,10 8,6.5"/></g></g></svg>
"""
pauseSVG="""<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
"""
stopSVG="""<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 8v8H8V8h8m2-2H6v12h12V6z"/></svg>
"""

@unsync
def unsync_func1(name):
    global stop_unsync1
    print('unsync1', name)
    if name == 'play':
        stop_unsync1 = False
        while 1:
            if stop_unsync1 == True:
                break
            time_unsync1 = st("%H:%M:%S")
            unsync_out1.object = f"<h1>{time_unsync1}</h1>"
            sl(0.1)
            # print('unsync1 running...')
    elif name == 'pause':
        stop_unsync1 = True
    elif name == 'stop':
        stop_unsync1 = True
        unsync_out1.object = "<h1>00:00:00</h1>"

@unsync
def unsync_func2(name):
    global stop_unsync2
    print('unsync2', name)
    if name == 'play':
        stop_unsync2 = False
        while 1:
            if stop_unsync2 == True:
                break
            time_unsync2 = st("%H:%M:%S")
            unsync_out2.object = f"<h1>{time_unsync2}</h1>"
            sl(0.1)
            # print('unsync2 running...')
    elif name == 'pause':
        stop_unsync2 = True
    elif name == 'stop':
        stop_unsync2 = True
        unsync_out2.object = "<h1>00:00:00</h1>"

@unsync
def unsync_func3(name):
    global stop_unsync3
    print('unsync3', name)
    if name == 'play':
        stop_unsync3 = False
        while 1:
            if stop_unsync3 == True:
                break
            time_unsync3 = st("%H:%M:%S")
            unsync_out3.object = f"<h1>{time_unsync3}</h1>"
            sl(0.1)
            # print('unsync3 running...')
    elif name == 'pause':
        stop_unsync3 = True
    elif name == 'stop':
        stop_unsync3 = True
        unsync_out3.object = "<h1>00:00:00</h1>"

class SVGButton(pn.reactive.ReactiveHTML):
    svg = param.String(doc="The SVG")
    name = param.String(doc='Icon ID')
    func_type = param.String(doc='Type of function')

    _template = """<button class="button" type="button" id="button" onclick="${_do}">{{svg}}</button>"""

    def _do(self, _):
        if self.func_type == 'unsync1':
            unsync_func1(self.name)
        elif self.func_type == 'unsync2':
            unsync_func2(self.name)
        elif self.func_type == 'unsync3':
            unsync_func3(self.name)

play_button_unsync1 = SVGButton(svg=playSVG, name='play', func_type='unsync1', height=60, width=60)
pause_button_unsync1 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync1', height=60, width=60)
stop_button_unsync1 = SVGButton(svg=stopSVG, name='stop', func_type='unsync1', height=60, width=60)
unsync_buttons1 = pn.Row(play_button_unsync1, pause_button_unsync1, stop_button_unsync1)

play_button_unsync2 = SVGButton(svg=playSVG, name='play', func_type='unsync2', height=60, width=60)
pause_button_unsync2 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync2', height=60, width=60)
stop_button_unsync2 = SVGButton(svg=stopSVG, name='stop', func_type='unsync2', height=60, width=60)
unsync_buttons2 = pn.Row(play_button_unsync2, pause_button_unsync2, stop_button_unsync2)

play_button_unsync3 = SVGButton(svg=playSVG, name='play', func_type='unsync3', height=60, width=60)
pause_button_unsync3 = SVGButton(svg=pauseSVG, name='pause', func_type='unsync3', height=60, width=60)
stop_button_unsync3 = SVGButton(svg=stopSVG, name='stop', func_type='unsync3', height=60, width=60)
unsync_buttons3 = pn.Row(play_button_unsync3, pause_button_unsync3, stop_button_unsync3)

unsync_out1 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")
unsync_out2 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")
unsync_out3 = pn.pane.HTML(object="""<h1>00:00:00</h1>""")

card_bg='rgba(255,255,255,0.33)'

gs = GridStack(sizing_mode='stretch_both', ncols=5, nrows=3, height=500, allow_resize=False, allow_drag=False)

gs[0:1, 0:5] = pn.Spacer(margin=5)
gs[1:2, 1:2] = pn.Card(unsync_out1, title='Unsync Function 1', background=card_bg, collapsible=False)
gs[1:2, 2:3] = pn.Card(unsync_out2, title='Unsync Function 2', background=card_bg, collapsible=False)
gs[1:2, 3:4] = pn.Card(unsync_out3, title='Unsync Function 3', background=card_bg, collapsible=False)
gs[2:3, 1:2] = unsync_buttons1
gs[2:3, 2:3] = unsync_buttons2
gs[2:3, 3:4] = unsync_buttons3

mt = pn.template.MaterialTemplate(
    header_background='rgba(255,255,255,0.33)',
    title='Unsync Demo',
    main=[gs],
).show()

1 Like

Thanks @danmaty. I moved the discussion here Provide example of asynchronous and concurrent tasks using unsync · Issue #2792 · holoviz/panel (github.com) to start turning this into a Panel gallery example or some Panel documentation.

I don’t have an example, but I just wanted to share something I found:

This seems useful too!

2 Likes

Here is a little example using unsync or asyncify for running a background task periodically.

Very basic panel app, which fetches the latest quotes (updating every 60 seconds) for all S&P500 companies from yahoo finance, store it in a pandas dataframe and and allows to query the dataframe with SQL using duckdb.

You can test unsync and asyncify by (un)commenting the respective functions.

Hope this example is useful for others.

import requests
import pandas as pd
import duckdb
import datetime
import panel as pn
from unsync import unsync
from asyncer import asyncify


pn.extension()

# GLOBALS
DB = duckdb.connect()
SP500_COMPANIES = pd.read_html(
    "https://en.wikipedia.org/wiki/List_of_S%26P_500_companies"
)[0]
SYMBOLS = sorted(SP500_COMPANIES["Symbol"].tolist())
QUOTE_URL = url = "https://query2.finance.yahoo.com/v6/finance/quote"
HEADERS = {"user-agent": "panel app"}
PARAMS = dict(symbols=",".join(SYMBOLS))

# WIDGETS
quotes = pn.widgets.DataFrame(height=400)
editor = pn.widgets.Ace(
    value="""SELECT * FROM quotes 
        WHERE (regularMarketPrice/regularMarketDayLow)>1.04 
        ORDER BY regularMarketChangePercent DESC""", # Market Price more than 4% above Day Low
    sizing_mode="stretch_width",
    language="sql",
    height=100,
    read_only=True,
)
results = pn.widgets.Tabulator(page_size=25, height=800, sizing_mode="stretch_width", pagination="local")
update_quotes = pn.widgets.Button(name="Update Quotes")
run_query = pn.widgets.Button(name="Run Query")
latest_update = pn.widgets.StaticText()

# FUNCTIONS
# @unsync
# def get_quotes(event=None):
#     response = requests.get(QUOTE_URL, params=PARAMS, headers=HEADERS).json()
#     quotes.value = (
#         pd.DataFrame(response["quoteResponse"]["result"])
#         .set_index("symbol")
#     )
#     latest_update.value = datetime.datetime.now().strftime("%c")

def _get_quotes(event=None):
    response = requests.get(QUOTE_URL, params=PARAMS, headers=HEADERS).json()
    quotes.value = (
        pd.DataFrame(response["quoteResponse"]["result"])
        .set_index("symbol")
    )
    latest_update.value = datetime.datetime.now().strftime("%c")

async def get_quotes(event=None):
    await asyncify(_get_quotes)(event)

pn.state.add_periodic_callback(get_quotes, period=60000,)
update_quotes.on_click(get_quotes,)


@pn.depends(run_query)
def update_results(_run_query):
    if isinstance(quotes.value, pd.DataFrame):
        
        DB.register("quotes", quotes.value.reset_index())
        res = DB.execute(editor.value).df()
        results.value = res


# APP
app = pn.template.BootstrapTemplate(
    title="S&P500 Intraday Scanner",
    header_background="black",
    sidebar=[pn.WidgetBox(update_quotes, "Latest Update", latest_update), pn.Column(update_results)],
    main=[
        #pn.Row("# S&P500 Intraday Scanner"),
        
        pn.Column(editor, run_query,sizing_mode="stretch_width"),
        results,
    ]
)

app.servable()

I´ve got a questions regarding my example:

Is there a way, to initialize the “get_quotes” functions somehow? Currently, I have to run it once by clicking the “Update Quotes” button.

2 Likes

Hi @legout

What I normally do with callbacks is I initiate them like

cb = pn.state.add_periodic_callback(refresh, period=1, start=True)

Then in the function that’s being periodically called I put something like

if cb.period != 60000:
    cb.period = 60000

This will ensure your data is loaded into your viz with no initial 60sec delay, but after that it will update only every 60 seconds.

Then you might want to add other buttons to your app to manipulate your CB like

cb.stop

or

if not cb.is_running:
    cb.start

etc, etc.

Hope this helps.

1 Like

Great solution.

Thanks! :slight_smile: