8.15. Valkuilen

Dezelfde patronen die asyncio prettig maken – geen preemptie, expliciete awaits – geven het ook een eigen verzameling valstrikken. Deze pagina is de catalogus van diegene die vaak genoeg voorkomen om de moeite waard te zijn om te kennen.

8.15.1. Vergeten te awaiten

Het aanroepen van een async def-functie geeft een coroutine-object terug. Het draait de body van de functie niet. Om die daadwerkelijk uit te voeren, moet de coroutine geawaitd worden of in een taak verpakt worden:

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

De bug is stil – het coroutine-object wordt aangemaakt, weggegooid en nooit uitgevoerd. De applicatie gaat verder alsof alles werkte. MicroPython logt soms een waarschuwing dat een coroutine nooit geawait is; soms doet het dat niet. Controleer op ontbrekende awaits bij elke aanroepplek die op een functieaanroep lijkt.

8.15.2. Strakke lussen zonder await

Een coroutine die in een lus draait en nooit awaitt, monopoliseert de event loop. Geen enkele andere taak boekt vooruitgang totdat de lus eindigt of de controle teruggeeft:

async def counter():
    n = 0
    while True:
        n += 1               # bug: starves the loop

De oplossing is een yield binnen de lus – doorgaans await asyncio.sleep_ms(0) – zodat andere gereedstaande taken een kans krijgen om te draaien. Rekenintensief werk hoort ook in die vorm thuis: een beeldverwerkingslus die honderden milliseconden per iteratie draait, zou ten minste eenmaal per iteratie de controle moeten teruggeven, zodat de rest van het programma niet stilvalt.

8.15.3. Het inslikken van CancelledError

De pagina annulering behandelde dit al uitvoerig. We herhalen het hier omdat het de meest voorkomende oorzaak is van “mijn applicatie wil niet afsluiten”: een coroutine vangt asyncio.CancelledError op voor opruimdoeleinden en vergeet deze opnieuw op te werpen. De taak blijft draaien; de aanroeper die om de annulering vroeg, blijft voor eeuwig hangen in afwachting dat deze klaar is. Werp de uitzondering altijd opnieuw op na het opruimen, of gebruik een try/finally-blok in plaats van een expliciete except.

8.15.4. Gedeelde status muteren over awaits heen

Coöperatieve planning garandeert dat een coroutine de CPU tussen awaits voor zichzelf heeft, maar zodra deze awaitt, kan een andere coroutine draaien. Als twee coroutines dezelfde datastructuur wijzigen in stappen die een await bevatten, kunnen hun bewerkingen op manieren in elkaar grijpen die de structuur beschadigen:

# bug: two tasks running do_work simultaneously can
# interleave around the await and corrupt items
async def do_work():
    n = len(items)
    await asyncio.sleep_ms(0)
    items.append(some_work(n))

Voor status die alleen tussen awaits binnen één coroutine wordt gemuteerd, is geen synchronisatie nodig. Voor status die over awaits heen wordt gemuteerd en vanuit meer dan één coroutine wordt benaderd, verpak je het kritieke gedeelte in een Lock.

8.15.5. await op moduleniveau

await is alleen geldig binnen een async def-body. Het op moduleniveau schrijven – buiten elke coroutine – is een syntaxisfout:

# bug: not inside an async def
result = await fetch()

De oplossing is om het werk in een coroutine te plaatsen en deze aan te roepen vanuit het asyncio.run()-startpunt van het programma.

8.15.6. Meerdere asyncio.run-aanroepen

MicroPython heeft één event loop. Het twee keer achter elkaar aanroepen van asyncio.run() – eenmaal voor de opzet, eenmaal voor het hoofdwerk – gebruikt nog steeds dezelfde loop. Het van binnenuit een draaiende coroutine aanroepen is een fout: de loop draait al. Beide gevallen komen het vaakst voor wanneer een script organisch groeit en de auteur het probeert uit te breiden door meer run()-aanroepen toe te voegen in plaats van het nieuwe werk in de bestaande main op te nemen.

8.15.7. Gebruik van Event vanuit een interrupt

asyncio.Event.set() is alleen veilig aan te roepen van binnen de event loop. Het aanroepen vanuit een GPIO-interrupthandler is een corruptierisico. Om een taak vanuit een interrupt te wekken, gebruik je in plaats daarvan ThreadSafeFlag – de pagina erover behandelt de vorm.

8.15.8. Lange synchrone aanroepen

Een coroutine kan asyncio’s eigen wachtprimitieven awaiten; al het andere dat deze aanroept draait synchroon en blokkeert de loop totdat het terugkeert. Een blokkerende time.sleep() van 200 ms, een SD-kaartschrijfactie die 80 ms duurt om weg te schrijven, een grote JPEG-compressie, een aanroep van csi.CSI.snapshot() – elk daarvan houdt de event loop voor de volledige duur vast. De oplossing hangt af van de aanroep:

  • Voor time.sleep: vervang het door await asyncio.sleep of await asyncio.sleep_ms.

  • Voor csi.CSI.snapshot: gebruik de async snapshot-wrapper die de opnamepagina bouwt.

  • Voor langdurige berekeningen (beeldverwerking, JPEG-codering): accepteer de kosten of breek het werk op in stukken die tussen iteraties door awaiten.

Asyncio kan een synchrone aanroep niet niet-blokkerend maken. Het kan alleen andere coroutines laten draaien terwijl iets anders aan het awaiten is.