MeVisLab Scripting Reference
Python async/await

Introduction

Since Python 3.5, it is possible to write asynchronous code in Python in a nice sequential way, without the tedious callbacks/lambdas. Starting with MeVisLab 3.2, it has become possible to use Python coroutines and the async/await keywords.

Here is a simple example (which is not very useful but shows the principle):

async def test():
print("called")
for i in range(10):
await asyncio.sleep(1)
print("waited 1 second")

As you can see from the code, you declare a coroutine (a function that will be able to run asynchronously) using the async keyword.

When you run this coroutine (e.g. by pressing a MDL Button with command = test), you will see that the method does NOT block the GUI while sleeping. Instead, the coroutine awakes with the complete closure and stack after each asyncio.sleep(1).

Note that you can only use await in functions/methods that are declared as async. This is a general principle of this pattern, because a coroutine is fundamentally different from a normal function, since it can capture the local execution state and re-enter the code with the same state later on.

Up to now, we have only been waiting for asyncio.sleep, what else can we await?

Awaitables

The following things can be awaited:

  • field changes (with and without timeout)
  • RemoteCallInterface RPC calls
    • on the caller side
    • on the remote side
  • asyncio.sleep (as a replacement for callLater)

You can find usage examples for the above in the MeVisLab module TestAsyncPython.

In general, every mechanism that supports asynchronous callbacks can be await'ed. To make this work, Python provides a future class that can be used as a bridge from callback to await based code.

Waiting for Field changes

So now let's have a look at waiting for a field change:

import mlabAsync
async def waitForSomeField():
# wait for the change (which needs to be triggered by someone):
await mlabAsync.fieldChanged(ctx, "fieldToWatch")
print("field changed")

Now, if you run waitForSomeField(), it will wait (without blocking) until the given field is changed by some asynchronous code. Examples for such fields are the BackgroundTask derived ML modules, which will emit a field change when they have finished their task. With the code above, you can wait for such an asynchronous event without needing to split your code into a callback and a second function to be called after the field change. Notice the mlabAsync import, this is a small Python module that we wrote to facilitate waiting for MeVisLab related asynchronous events.

Waiting for RPC calls

The RemoteCallInterface base interface is used in many MeVisLab-based Web applications or when talking to remote modules in a mlab network. The RPC interface is asynchronous, so when you wanted to get the result of a RPC call, you needed to pass a result callback to get the result, like this:

# OLD STYLE RPC call
def callSomething():
_rpc.callWithResult(resultCallback, "Whatever.someMethod")
def resultCallback(result):
print(result)

Now, with async/wait, you can use the new asyncCall method on the RemoteCallInterface:

async def callSomething():
result = await _rpc.asyncCall("Whatever.someMethod")
print(result)

When having to do a single call, the difference it not that obvious, but with the async/await pattern, you can do multiple such calls in a sequential way, which in the background everything executes asynchronously:

async def callSomething():
result1 = await _rpc.asyncCall("Whatever.someMethod")
result2 = await _rpc.asyncCall("Whatever.someOtherMethod", [result1])
result3 = await _rpc.asyncCall("Whatever.andAnotherMethod", [result2])
print(result3)

The above would already look quite ugly with callbacks.

Note that both the client side (asyncCall side) and the server side (the receiver of the asyncCall) support async/await. This means that the RPC method that you call on the remote side can also be an async method and await things. The result of the call will be returned when the async call has finished awaiting everything.