A crash course on asyncio#
Ophyd-async uses Python’s asyncio library to communicate with many pieces of hardware without blocking while any one of them responds. This page gives you enough asyncio vocabulary to follow the ophyd-async tutorials. It assumes that you can already write and call ordinary Python functions.
By the end, you will be able to:
recognize the async syntax used by ophyd-async;
decide whether operations should happen in sequence or concurrently; and
enter async code correctly from a script, IPython, a test, or a Bluesky plan.
Why ophyd-async uses asyncio#
Hardware control contains a lot of waiting: a network request is in flight, a motor is moving, or a detector is acquiring. Threads can overlap those waits, but coordinating many threads requires locks and other synchronization. Asyncio instead makes the places where code may wait explicit. While one operation is waiting, an event loop can let another operation make progress.
Ophyd-async normally gets this concurrency from one event-loop thread, so it does not need one operating-system thread per device operation. This also makes interruption and cleanup more predictable. For a direct comparison with threads, see Design goals and differences with ophyd sync.
Asyncio is best suited to this kind of input/output (I/O) concurrency. It does not make long CPU-bound calculations run in parallel. A blocking function also blocks the event-loop thread, so use async library calls inside async code.
Define and await a coroutine#
An async def statement defines a coroutine function. Calling it creates a
coroutine object, which represents work that can be run later. It does not run
the function immediately.
import asyncio
async def read_sensor(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name} ready"
async def main() -> None:
result = await read_sensor("detector", 0.1)
print(result)
asyncio.run(main())
The await expression waits for an awaitable such as a coroutine, an
asyncio.Task, or many ophyd-async status objects. If that operation needs to
wait, the current coroutine is suspended and the event loop can run other work.
When the operation finishes, await produces its result or raises its exception.
Calling read_sensor(...) without awaiting or scheduling the returned coroutine
would not run it. Python will normally warn that the coroutine was never awaited.
Except for environments that support top-level await, use await inside an
async def function.
Call synchronous hardware APIs#
Some third-party hardware libraries provide only blocking, synchronous methods.
Calling one directly from a coroutine stops every task on the event-loop thread
until it returns. asyncio.to_thread lets the function run in a separate
thread while the event loop continues:
reading = await asyncio.to_thread(blocking_detector.read)
Pass the function itself to to_thread(), followed by any positional or keyword
arguments, rather than calling the function first:
await asyncio.to_thread(blocking_motor.move, position, timeout=5)
Prefer an async API when the library provides one. Otherwise, use to_thread()
for blocking I/O after checking that the library supports calls from a worker
thread. It is not normally a way to parallelize CPU-bound Python code.
Choose sequence or concurrency#
Two await expressions in a row are sequential. The second operation starts
after the first has finished:
temperature = await temperature_signal.get_value()
pressure = await pressure_signal.get_value()
When operations are independent, asyncio.gather can run them concurrently:
temperature, pressure = await asyncio.gather(
temperature_signal.get_value(),
pressure_signal.get_value(),
)
On success, gather() returns results in the same order as its arguments, even
if the operations finish in a different order. An exception is raised to the
code awaiting gather().
Use sequential awaits when one operation depends on the previous result. Use
gather() only when the operations are independent and the hardware supports
them happening at the same time. Concurrency changes ordering; it is not merely
a performance switch.
Recognize the other async keywords#
Async code has two more forms that allow waiting at points where ordinary Python cannot:
async withenters and exits an asynchronous context manager. Either step may await setup or cleanup. For example,async withletsinit_devicesconnect devices when it is called from an async function:async with init_devices(): detector = MyDetector("DEVICE-PREFIX")
async forconsumes an asynchronous iterator. Each iteration may await the next value. Ophyd-async uses this pattern to observe signal updates:async for value in observe_value(signal): print(value)
The ordinary with and for forms are still appropriate when entering,
exiting, or advancing cannot require an async wait.
Enter async code in the right place#
How you start async work depends on the environment:
Script: define one outer
main()coroutine and callasyncio.run(main()). This creates an event loop, runsmain(), and closes the loop. Do not callasyncio.run()from code that is already inside a running event loop.IPython: use top-level
await, such asawait detector.read(). When an IPython session also has a Bluesky RunEngine,autoawait_in_bluesky_event_loop()makes top-level awaits use the RunEngine’s event loop. The Using Devices tutorial configures this for you.Pytest: write an
async def test_...and await device methods inside it. The Writing Tests for Devices tutorial shows the requiredpytest-asyncioconfiguration.Bluesky plan: plans are generator functions driven by the RunEngine. Use
yield fromwith Bluesky plan stubs rather than directly awaiting device methods. The RunEngine coordinates the plan with ophyd-async’s async device operations.
Avoid common mistakes#
If you see
coroutine was never awaited, find the coroutine call and either await it or deliberately schedule and later await it.Do not use
time.sleep()in async code; it blocks every task on the event-loop thread. Useawait asyncio.sleep()when you need an async delay.Do not add
asyncio.run()around a call in IPython or other code that already has a running event loop. Useawaitthere.Do not make dependent or mutually exclusive hardware operations concurrent. Preserve their required order with separate
awaitexpressions.
You now have the asyncio concepts needed for Using Devices. For more device-specific patterns, see How to interact with signals and commands while implementing bluesky verbs. The Python asyncio documentation covers the full standard library.