Concurrent execution

Concert relies on concurrency instead of parallelism because what mostly happens is communication with devices, which is I/O bound. Concurrency is realized via coroutines and Python’s asyncio module. A coroutine is a function defined as async def and inside it can yield execution for other coroutines via the await keyword. When you call a coroutine function, it returns a coroutine object, but the code of that function is not yet executed. One way of invoking execution in a blocking way is by the await keyword followed by a coroutine object. This will block the session until the coroutine is finished. Alternatively, you can start the execution in a non-blocking way by calling start() and get the control back immediately. start() returns a task object, which can also be awaited. Most of the async def functions in Concert are wrapped into tasks by the background() decorator, so you do not need to use the start() in order to start execution immediately. You should however keep this in mind when writing your own coroutines and decorate them (see below) if you want them to be automatically started upon invocation. Overall, in Concert there are two ways to execute coroutines:

  1. as non-blocking tasks,

  2. as blocking tasks in combination with the await syntax

An example:

import asyncio
from concert.coroutines.base import background, start

async def corofunc():
    await asyncio.sleep(0.1)
    return 1

@background
async def corofunc_run_immediately():
    await asyncio.sleep(0.1)
    return 1

coro = corofunc() # coro is a coroutine, not yet a task and has not started
task = start(coro) # wraps the coroutine into a task and starts it, does not block
result = await task # this blocks, result contains 1
await corofunc() # this blocks too

task = corofunc_run_immediately() # runs immediately, does not block
result = await task # this blocks, result contains 1
await corofunc_run_immediately() # this blocks too

A more reallistic example:

from concert.devices.motors.dummy import LinearMotor

motor = await LinearMotor()
task = motor.home() # this doesn't block
await task # this blocks
await motor.home() # this blocks too

You can cancel running tasks which are being awaited by pressing ctrl-c. This for instance stops a motor. By ctlr-k, you can also cancel all running background tasks which were started by the start() function or background() decorator. On the top of cancellation, ctrl-k will call Device.emergency_stop() on all devices in order to bring them to a standstill. Please note that if there is non-async function running, ctrl-k will only get triggered after it has finished. You can first cancel the running operation byt ctrl-c followed by ctrl-k to execute it as soon as possible.

Concurrency

Concurrent execution itself is realized via asyncio’s tools, like gather, which executes given coroutines concurrently and returns their results:

async def corofunc():
    await asyncio.sleep(0.1)
    return 1

await asyncio.gather(corofunc(), corofunc())

Synchronization

When using the concurrent getters and setters of Device and Parameter, coroutines can not be sure if other coroutines manipulate the device. To lock devices or specific parameters, coroutines can use devices with context managers:

async with shutter, motor['position']:
    await motor.set_position(2 * q.mm)
    await shutter.open()

Inside the async with environment, a coroutine has exclusive access to the devices and parameters.