11.15. Pitfalls¶
The same patterns that make asyncio nice – no preemption,
explicit awaits – give it its own set of shapes that
bite. This page is the catalogue of the ones that come up
often enough to be worth knowing about.
11.15.1. Forgetting to await¶
Calling an async def function returns a coroutine
object. It does not run the body of the function. To
actually execute it, the coroutine has to be awaited
or wrapped in a task:
async def main():
send_request() # bug: returns the coroutine, does nothing
await send_request() # right: run it to completion
asyncio.create_task(send_request()) # right: run it concurrently
The bug is silent – the coroutine object is created,
discarded, and never executed. The application proceeds as
if everything worked. MicroPython will sometimes log a
warning that a coroutine was never awaited; sometimes it
will not. Audit for missing awaits at every call site
that looks like a function call.
11.15.2. Tight loops without await¶
A coroutine that runs in a loop and never awaits
monopolises the event loop. No other task makes progress
until the loop exits or yields:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
The fix is a yield inside the loop – usually
await asyncio.sleep_ms(0) – so other ready tasks get a
chance to run. Computation-heavy work belongs inside that
shape too: an image-processing loop that runs for hundreds
of milliseconds per iteration should yield at least once
per iteration so the rest of the program does not stall.
11.15.3. Swallowing CancelledError¶
The cancellation page already covered
this in detail. Repeating it here because it is the most
common cause of “my application will not shut down”: a
coroutine catches asyncio.CancelledError for cleanup
purposes and forgets to re-raise it. The task continues to
run; the caller that asked for the cancellation hangs
forever waiting for it to finish. Always re-raise after
cleanup, or use a try/finally block instead of an
explicit except.
11.15.5. Module-level await¶
await is only valid inside an async def body.
Writing it at module level – outside any coroutine – is a
syntax error:
# bug: not inside an async def
result = await fetch()
The fix is to put the work in a coroutine and call it from
the program’s asyncio.run() entry point.
11.15.6. Multiple asyncio.run calls¶
MicroPython has one event loop. Calling
asyncio.run() twice in a row – once for setup, once
for the main work – still uses the same loop. Calling it
from inside a running coroutine is an error: the loop is
already running. Both cases come up most often when a
script grows organically and the author tries to extend it
by adding more run() calls instead of
folding the new work into the existing main.
11.15.7. Use of Event from an interrupt¶
asyncio.Event.set() is only safe to call from inside
the event loop. Calling it from a GPIO interrupt handler is
a corruption hazard. For waking a task from an interrupt,
use ThreadSafeFlag instead – the
page on it covers the
shape.
11.15.8. Long synchronous calls¶
A coroutine can await asyncio’s own waiting primitives;
anything else it calls runs synchronously and blocks the
loop until it returns. A 200 ms blocking
time.sleep(), an SD-card write that takes 80 ms to
flush, a large JPEG compression, a csi.CSI.snapshot()
call – each of those holds the event loop for its full
duration. The fix depends on the call:
For
time.sleep: replace it withawait asyncio.sleeporawait asyncio.sleep_ms.For
csi.CSI.snapshot: use the async snapshot wrapper the capture page builds.For long compute (image processing, JPEG encode): accept the cost or break the work into chunks that
awaitbetween iterations.
Asyncio cannot make a synchronous call non-blocking. It can only let other coroutines run while something else is awaiting.