8.15. Fallgropar

Samma mönster som gör asyncio trevligt – ingen förmånsavbrytning, explicita awaits – ger det sin egen uppsättning former som biter. Den här sidan är katalogen över de som dyker upp tillräckligt ofta för att vara värda att känna till.

8.15.1. Att glömma await

Att anropa en async def-funktion returnerar ett coroutine-objekt. Det kör inte funktionens kropp. För att faktiskt köra den måste coroutinen awaitas eller paketeras i en uppgift:

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

Buggen är tyst – coroutine-objektet skapas, kastas bort och körs aldrig. Applikationen fortsätter som om allt fungerade. MicroPython loggar ibland en varning om att en coroutine aldrig inväntades; ibland gör den det inte. Granska efter saknade awaits vid varje anropsställe som ser ut som ett funktionsanrop.

8.15.2. Snäva loopar utan await

En coroutine som körs i en loop och aldrig awaitar monopoliserar händelseloopen. Ingen annan uppgift gör framsteg förrän loopen avslutas eller lämnar över:

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

Lösningen är ett yield inuti loopen – vanligtvis await asyncio.sleep_ms(0) – så att andra redo uppgifter får en chans att köra. Beräkningstungt arbete hör också hemma inuti den formen: en bildbehandlingsloop som körs i hundratals millisekunder per iteration bör lämna över minst en gång per iteration så att resten av programmet inte stannar upp.

8.15.3. Att svälja CancelledError

Sidan om avbrytande täckte redan detta i detalj. Det upprepas här eftersom det är den vanligaste orsaken till ”min applikation vill inte stänga ned”: en coroutine fångar asyncio.CancelledError för uppstädning och glömmer att höja om det. Uppgiften fortsätter att köra; anroparen som bad om avbrytandet hänger sig för evigt och väntar på att den ska bli klar. Höj alltid om efter uppstädning, eller använd ett try/finally-block istället för ett explicit except.

8.15.4. Att mutera delat tillstånd över awaits

Kooperativ schemaläggning garanterar att en coroutine har CPU:n för sig själv mellan awaits, men så snart den awaitar kan en annan coroutine köra. Om två coroutiner modifierar samma datastruktur i steg som inkluderar await kan deras operationer flätas samman på sätt som korrumperar strukturen:

# 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))

För tillstånd som bara muteras mellan awaits inuti en coroutine behövs ingen synkronisering. För tillstånd som muteras över awaits och nås från fler än en coroutine, paketera den kritiska sektionen i ett Lock.

8.15.5. await på modulnivå

await är endast giltigt inuti en async def-kropp. Att skriva det på modulnivå – utanför någon coroutine – är ett syntaxfel:

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

Lösningen är att lägga arbetet i en coroutine och anropa den från programmets ingångspunkt asyncio.run().

8.15.6. Flera asyncio.run-anrop

MicroPython har en händelseloop. Att anropa asyncio.run() två gånger i rad – en gång för uppstart, en gång för huvudarbetet – använder fortfarande samma loop. Att anropa det inifrån en körande coroutine är ett fel: loopen körs redan. Båda fallen dyker oftast upp när ett skript växer organiskt och författaren försöker utöka det genom att lägga till fler run()-anrop istället för att vika in det nya arbetet i det befintliga main.

8.15.7. Användning av Event från ett avbrott

asyncio.Event.set() är endast säkert att anropa inifrån händelseloopen. Att anropa det från en GPIO-avbrottshanterare är en korruptionsrisk. För att väcka en uppgift från ett avbrott, använd istället ThreadSafeFlagsidan om det täcker formen.

8.15.8. Långa synkrona anrop

En coroutine kan invänta asyncios egna väntprimitiver; allt annat den anropar körs synkront och blockerar loopen tills det returnerar. En 200 ms blockerande time.sleep(), en SD-kortsskrivning som tar 80 ms att tömma, en stor JPEG-komprimering, ett anrop till csi.CSI.snapshot() – var och en av dessa håller händelseloopen under hela sin varaktighet. Lösningen beror på anropet:

  • För time.sleep: ersätt det med await asyncio.sleep eller await asyncio.sleep_ms.

  • För csi.CSI.snapshot: använd den asynkrona snapshot-omslagsklass som fångstsidan bygger.

  • För lång beräkning (bildbehandling, JPEG-kodning): acceptera kostnaden eller dela upp arbetet i delar som awaitar mellan iterationerna.

Asyncio kan inte göra ett synkront anrop icke-blockerande. Det kan bara låta andra coroutiner köra medan något annat inväntas.