11.2. Coroutines and tasks¶
Coroutines are the unit of work an asyncio program is built from; tasks are how an application runs several coroutines concurrently.
11.2.1. Coroutines¶
A coroutine is a function declared with async def:
import asyncio
async def heartbeat(interval_ms):
while True:
print("tick")
await asyncio.sleep_ms(interval_ms)
The body looks like an ordinary function, with one extra
ingredient: await. Wherever the coroutine has to wait
for something – a sleep, a network read, an event being set
– it awaits an expression that knows how to suspend
the coroutine until the thing it is waiting for is ready.
At each await the coroutine hands control back to
asyncio; asyncio resumes it from the same point once the
awaited operation has completed.
The asyncio module ships two sleeps:
asyncio.sleep()– argument in seconds, accepts a float.asyncio.sleep_ms()– argument in milliseconds, takes an int. A MicroPython extension; usually the right choice on the camera because timing knobs in firmware are millisecond-shaped.
A bare async def does nothing on its own. Calling
heartbeat(500) does not execute the body; it returns a
coroutine object that asyncio has to schedule. The
simplest way to schedule one is asyncio.run():
asyncio.run(heartbeat(500))
asyncio.run() starts the event loop, schedules the
coroutine it was handed as the top-level entry point, drives
the loop until that coroutine returns, then tears the loop
down. For a single coroutine that’s the whole program. For
several coroutines the application reaches for tasks.
11.2.2. Tasks¶
A task is asyncio’s wrapper around a coroutine that says
schedule this concurrently with the current one and let me
keep going. asyncio.create_task() makes one and
returns a Task object representing the
scheduled work:
task = asyncio.create_task(heartbeat("fast", 100))
The coroutine is now on the loop’s schedule; the caller has
not waited for it. The returned Task is
the handle the caller uses afterwards to interact with that
running work.
Once the application has the handle it can do three things with it:
Wait for the task to finish. A
Taskis itself awaitable.result = await tasksuspends the current coroutine untiltask’s coroutine returns, then resumes with whatever that coroutine returned (or re-raises whatever it raised).Cancel the task.
task.cancel()schedulesasyncio.CancelledErrorto be raised inside the task’s coroutine at its nextawait, giving it a chance to run cleanup code in afinallyblock. The page on timeouts and cancellation covers the details.Identify it later.
asyncio.current_task()returns theTaskfor the coroutine that is currently running. Most scripts never call it; it shows up in instrumentation and in exception handlers.
The script does not have to capture the handle every time. Throwaway background tasks the application starts and leaves running can drop the return value – the loop still schedules them:
import asyncio
async def heartbeat(name, interval_ms):
while True:
print(name)
await asyncio.sleep_ms(interval_ms)
async def main():
asyncio.create_task(heartbeat("fast", 100))
asyncio.create_task(heartbeat("slow", 500))
await asyncio.sleep(5)
asyncio.run(main())
The two create_task calls schedule both heartbeats
without waiting for either of them. Control returns
immediately to main, which then awaits a five-second
sleep. While it sleeps the two heartbeat tasks make
progress; the loop cycles through whichever task is ready
to run. After five seconds main returns, the loop tears
down whatever tasks are still alive, and asyncio.run()
returns to the caller.
Capture the handle whenever the application actually needs one of the three operations above. In practice that means almost always, because cleanly shutting an application down means cancelling the background tasks it spawned – the cancellation page covers the pattern.
11.2.3. The two-line rule¶
The minimal asyncio program is the two lines the examples above end with:
async def main():
...
asyncio.run(main())
Everything else – the tasks the application creates, the
primitives it coordinates them with, the streams it opens
– happens inside main (and inside the coroutines
main spawns). When a script outgrows the camera’s
classic while True: csi0.snapshot() loop, the answer is
not to call asyncio.run() in several places; it is to
fold the new work into main as more tasks.