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.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 ThreadSafeFlag – sidan 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 medawait asyncio.sleepellerawait 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.