Skip to content

Instantly share code, notes, and snippets.

@freakboy3742
Last active December 5, 2017 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save freakboy3742/c6a67d6594f6e9c741ff4151a34ccc51 to your computer and use it in GitHub Desktop.
Save freakboy3742/c6a67d6594f6e9c741ff4151a34ccc51 to your computer and use it in GitHub Desktop.
Asyncio
import asyncio
async def stuff(val):
print("Going to sleep for", val)
await asyncio.sleep(val)
print("slept")
return val * 2
def do_stuff():
print("do stuff")
task = asyncio.ensure_future(stuff(1))
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print("Task result is", task.result())
async def main():
print("Start main")
do_stuff()
print("Main complete")
>>> do_stuff()
do stuff
Going to sleep for 1
slept
Task result is 2
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(main())
Traceback
...
RuntimeError: This event loop is already running
@freakboy3742
Copy link
Author

Background

Here's what I'm trying to do:

Toga is a cross-platform, Python-native GUI toolkit. As such, it's trying to preserve a highly Pythonic API. This means integrating the Cocoa/GTK/etc event loops with the asyncio event loop.

However, we also want Toga to be an "accessible to end users" API. This means hiding the eccentricities of specific backends and languages, and providing a clean, approachable and useful API.

What Toga can already do

When you set up a button in Toga, you can specify a handler to be invoked when that button is pressed. That handler can be a function, a method, a generator, or a coroutine. If you want a label to update every time a button is pressed, you can write a simple function that updates the label text. But if you're going to do something more complicated (say, run a background process, or call an external API), you can use an coroutine, and the app won't lock up, because control will be released to the app's event loop unless there is actual work to be done in the handler.

The key feature: Handlers are not required to be coroutines. This means we don't have to teach beginners all about asynchronous programming just to get them going. They can use normal functions and methods, like they're familiar with, up until they have a complex requirement.

The specific problem

One of the Toga widgets is a WebView, so you can drop a HTML browser into your app.

The WebView widget has a method called "evaluate()". This method takes a string containing Javascript, runs it in the web view, and returns the Javascript result as a string.

Cocoa (the underlying native widget toolkit on macOS) is in the process of changing it's web view widget. The old widget (WebView) has a method stringByEvaluatingJavaScriptFromString: that does exactly this. However, the new widget (WKWebView) doesn't do this - but it does have a evaluateJavascript:completionHandler: method - an asynchronous method to inject the javascript, and set a callback method to be invoked when the JavaScript finishes running.

This is, by all accounts, a much better API for Cocoa. There's no way to know how long a block of javascript will take to execute; using a callback means the Cocoa app doesn't lock up if the Javascript takes a long time to execute. It's also the best option available, because Objective-C as a language doesn't have any asyncio-like features.

However, Python does. So this new API poses a challenge for Toga. We want to provide an externally synchronous API, hiding the underlying internally asynchronous detail. We want the user to write a handler method that just invokes "evaluate()", which calls the underlying cocoa API, and releases control to the event loop while we wait for the response; then when the result is ready, the user's handler is resumed exactly where it left off.

The user shouldn't need to understand asynchronous programming to be able to call a method, get a result, and do something with the result.

The simplified example

The example code provided is a simplified version of this problem. do_stuff() is an external API. It's a method, not a co-routine. It can be called, and it will execute the coroutine that does the actual work (stuff), and get back a result when the async method finishes running.

If a user wrote some code, they could "just invoke" do_stuff(), and it would operate asynchronously. Ok, in this case, it honestly doesn't matter, because there's nothing else going on - but this is also a deliberately simplified example.

It can do this because it calls run_until_complete(). This runs the event loop until the co-routine completes.

However, if the event loop already exists - if, for example, it is invoked from inside the main() coroutine - it fails, because you can't start a second event loop.

I know run_until_complete() isn't the right answer - if only because it raises an error. But that's the behavior I want. I want to "release control to the event loop that already exists, until such time as a return value is available."

What I don't want

I know I could "just make evaluate() a coroutine method".

I know I could "just make evaluate() return a Future".

I know I could "just expose a different API to the user so they pass in a callback method."

Listen carefully: I DON'T WANT TO DO ANY OF THESE THINGS.

If there is literally no other way to do this - if this is actually technically not possible for some reason, then I'll grudgingly make evaluate() a coroutine. But I'd really, REALLY like to avoid that if I can.

Conceptually, I don't see a reason why this can't be done. There is an existing event loop. I want to defer to it, and get back control when the work is done. As far as I can make out, this is exactly what asyncio is meant to achieve.

If I'm wrong - if there's a fundamental reason why this can't be done, then by all means let me know.

@ncoghlan
Copy link

ncoghlan commented Dec 4, 2017

Summarising the related Twitter discussion:

  1. You've run into the fundamental "What colour is my function?" problem that afflicts all explicitly async programming models: http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
  2. The first path out is to concede defeat, make evaluate() a coroutine, and require user handlers that want to call it also be coroutines
  3. The second path out is to run synchronous user handlers in a separate thread, such that evaluate() can be a synchronous API that uses https://docs.python.org/3/library/asyncio-task.html#asyncio.run_coroutine_threadsafe to delegate the call back to the original event loop
  4. Offer both, such that event handlers end up with 3 modes of execution:
    • synchronous in the main thread (can access other widgets without locks, but can't wait for events)
    • coroutine in the main thread (can access other widgets without locks, and can wait for events with await)
    • synchronous in a separate thread (can block waiting for events, but can't directly access or manipulate widget state)

I'm going to suggest that you eventually aim for option 4, and offer two evaluate() APIs:

  • WebView.evaluate_async(): the coroutine version that actually does the work in the main thread

  • WebView.evaluate(): a synchronous version that does roughly:

     def evaluate(self, *args, **kwds):
         if this_is_the_main_thread():
             raise RuntimeError("Handler must be decorated with '@toga.blocking' to use WebView.evaluate()")
         eval_cr = self.evaluate_async(*args, **kwds)
         eval_future = asyncio.run_coroutine_threadsafe(eval_cr, main_loop)
         return eval_future.result() # This blocks waiting for the main thread
    

To declare blocking synchronous handlers, toga would need to gain a new @toga.blocking decorator that worked something like the following:

def blocking(handler):
    @functools.wraps(handler):
    async def async_wrapper(widget):
        widget_proxy = WidgetProxy(widget)
        sync_call = functools.partial(handler, widget_proxy)
        return await asyncio.get_event_loop().run_in_executor(None, sync_call)
   return async_wrapper

The WidgetProxy helper would be needed to deal with the fact that, while synchronous handlers marked as blocking APIs would gain the freedom to block without blocking the main thread, they'd also gain the restriction of not being allowed to access API widgets directly, since doing so would be a good way to create race conditions.

I'm not familiar enough with toga internals to be able to fully design WidgetProxy here, but the conceptual heart of it would be similar to the suggested synchronous evaluate API above:

async def _method_coroutine(widget, method_name, args, kwds):
    return getattr(widget, method_name)(*args, **kwds)

def _call_widget_method_via_proxy(proxy, method_name, *args, **kwds):
    if this_is_the_main_thread():
        raise RuntimeError("Widget proxies are only needed for handlers decorated with '@toga.blocking'")
    method_cr = _method_coroutine(proxy._widget, args, kwds)
    method_future = asyncio.run_coroutine_threadsafe(method_cr, main_loop)
    return method_future.result() # This blocks waiting for the main thread

@freakboy3742
Copy link
Author

@ncoghlan Thanks for that. I was pointed at some vaguely similar code from @dabaez - there's definitely some room here for a decorator that lets us offer an async api, but provide a naïve synchronous interface for ease of use.

@dpnova
Copy link

dpnova commented Dec 4, 2017

I know it's not 100% the same, but crochet for twisted does something like this doesn't it? Just leave the event loop running in another thread and call in/out from it as required?

@dpnova
Copy link

dpnova commented Dec 4, 2017

Conceptually, I don't see a reason why this can't be done. There is an existing event loop. I want to defer to it, and get back control when the work is done. As far as I can make out, this is exactly what asyncio is meant to achieve.

I know Nick has covered all the meaty ground above, but I find this comment interesting. This implies something will block until you get a result back, but blocking a thread that also has the event loop in it means the event loop won't be able to run. I guess it could be possible if you use signals and timers from the OS, but that does mean the event loop won't be running in those blocking times. In fact I'm pretty sure twisted does something like this to get async behaviour on windows.. it does remind me of some stuff @glyph has written about in the past too: https://glyph.twistedmatrix.com/2014/02/unyielding.html

Anyway... interesting conversation :)

@futursolo
Copy link

@dpnova But I think that WKWebView must be registered with the event loop on the main(UI) thread.

Also, even if this task delegated to another thread, using any synchronous way to get the result will still block the main thread and freeze the GUI.

The intention of the original issue(beeware/toga#68) is that I want to effectively avoid cases like this to freeze the GUI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment