Asynchronous Programming

Asynchronous programming is an easy way to do several things at the same time. For example, your program can home two devices while a laser warms up. The operations run simultaneously instead of one after another, so the whole sequence finishes sooner.

Methods that communicate with devices have an asynchronous variant with the _async suffix, such as home_async. Calling it returns a coroutine, and await waits until it finishes. The examples in this guide require Python 3.12 or newer.

Example

The following program prepares an experiment. Two devices home and then move to their start positions while a laser initializes and powers on. The program assumes two devices connected to the same port.

import asyncio

from zaber_motion import Units
from zaber_motion.ascii import Axis, Connection


async def home_and_move(axis: Axis) -> None:
    await axis.home_async()
    await axis.move_absolute_async(10, Units.LENGTH_MILLIMETRES)


async def prepare_laser(laser: MockLaser) -> None:
    await laser.initialize()
    await laser.power_on()


async def main() -> None:
    async with Connection.open_serial_port_async("COM3") as connection:
        await connection.enable_alerts_async()

        device_list = await connection.detect_devices_async()
        axis1 = device_list[0].get_axis(1)
        axis2 = device_list[1].get_axis(1)

        await asyncio.gather(
            home_and_move(axis1),
            home_and_move(axis2),
            prepare_laser(MockLaser()),
        )


asyncio.run(main())
using Zaber.Motion;
using Zaber.Motion.Ascii;

await using (var connection = await Connection.OpenSerialPortAsync("COM3")) {
    await connection.EnableAlertsAsync();

    var deviceList = await connection.DetectDevicesAsync();
    var axis1 = deviceList[0].GetAxis(1);
    var axis2 = deviceList[1].GetAxis(1);

    await Task.WhenAll(
        HomeAndMoveAsync(axis1),
        HomeAndMoveAsync(axis2),
        PrepareLaserAsync(new MockLaser())
    );
}

async Task HomeAndMoveAsync(Axis axis)
{
    await axis.HomeAsync();
    await axis.MoveAbsoluteAsync(10, Units.Length_Millimetres);
}

async Task PrepareLaserAsync(MockLaser laser)
{
    await laser.InitializeAsync();
    await laser.PowerOnAsync();
}
import { Length } from '@zaber/motion';
import { Connection } from '@zaber/motion/ascii';

async function homeAndMove(axis) {
  await axis.home();
  await axis.moveAbsolute(10, Length.mm);
}

async function prepareLaser(laser) {
  await laser.initialize();
  await laser.powerOn();
}

async function main() {
  await using connection = await Connection.openSerialPort('COM3');
  await connection.enableAlerts();

  const deviceList = await connection.detectDevices();
  const axis1 = deviceList[0].getAxis(1);
  const axis2 = deviceList[1].getAxis(1);

  await Promise.all([
    homeAndMove(axis1),
    homeAndMove(axis2),
    prepareLaser(new MockLaser()),
  ]);
}

main();
import ZaberMotion
import ZaberMotionAscii

func homeAndMove(_ axis: Axis) async throws {
    try await axis.home()
    try await axis.moveAbsolute(position: 10, unit: Units.Length.mm)
}

func prepareLaser(_ laser: MockLaser) async throws {
    try await laser.initialize()
    try await laser.powerOn()
}

let connection = try await Connection.openSerialPort(portName: "COM3")
try await connection.enableAlerts()

let deviceList = try await connection.detectDevices()
let axis1 = try deviceList[0].getAxis(axisNumber: 1)
let axis2 = try deviceList[1].getAxis(axisNumber: 1)

async let firstDeviceReady: Void = homeAndMove(axis1)
async let secondDeviceReady: Void = homeAndMove(axis2)
async let laserReady: Void = prepareLaser(MockLaser())
_ = try await [firstDeviceReady, secondDeviceReady, laserReady]

try await connection.close()

MockLaser is a mock for illustrative purposes. It could just as well be any other instrument.

home_and_move and prepare_laser are asynchronous functions. Inside each of them, the steps run in order because each step is awaited. For example, the device starts moving only after homing finishes.

asyncio.gather starts all three functions at the same time and waits until all of them finish. If any of them fails, awaiting asyncio.gather raises the error.

asyncio.run(main()) starts the asynchronous code, and the async with statement closes the connection when the block ends.

If you await each of the functions separately instead, they run one after another, and the program takes as long as all the steps added together.

Timing and Order

Asynchronous programming saves time, but it does not synchronize the devices. The library sends each command separately, and communication and operating system delays vary. As a result, the devices start moving at slightly different moments, and the difference changes from run to run. For movements with consistent trajectory, use Streams or PVT.

Commands started at the same time can also reach the devices in any order. If one step must happen before another, await it first. See Thread safety for more details.

Calling Blocking Code

Blocking calls return only after their work is done, and they cannot be awaited. Examples are the synchronous methods of the library, such as home() instead of home_async(), and libraries without asynchronous methods, for example some camera APIs. While a blocking call runs, nothing else in your program runs. Devices finish the commands they already received, but the next steps of all other functions wait.

To run a blocking call alongside the other operations, pass it to asyncio.to_thread, which runs it in a separate thread:

Python
def connect_camera() -> None:
    ...  # Blocking call to your camera library

# ...

await asyncio.gather(
    home_and_move(axis1),
    home_and_move(axis2),
    prepare_laser(MockLaser()),
    asyncio.to_thread(connect_camera),
)