8.15. Úskalí#
Tytéž vzory, které dělají asyncio příjemným – žádné předbíhání, explicitní awaity – mu dávají vlastní sadu situací, které dokážou potrápit. Tato stránka je katalogem těch, které se objevují dostatečně často na to, aby se o nich vyplatilo vědět.
8.15.1. Zapomenutí na await#
Volání funkce async def vrací objekt korutiny. Nespustí tělo funkce. Aby se skutečně vykonalo, musí se na korutinu awaitovat nebo ji zabalit do úlohy:
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
Chyba je tichá – objekt korutiny se vytvoří, zahodí a nikdy nevykoná. Aplikace pokračuje, jako by vše fungovalo. MicroPython někdy zaloguje varování, že na korutinu nikdy nebylo počkáno; někdy ne. Při každém místě volání, které vypadá jako volání funkce, prověřte chybějící awaity.
8.15.2. Těsné smyčky bez await#
Korutina, která běží ve smyčce a nikdy awaituje, monopolizuje smyčku událostí. Žádná jiná úloha nepostoupí, dokud smyčka neskončí nebo nepředá řízení:
async def counter():
n = 0
while True:
n += 1 # bug: starves the loop
Řešením je yield uvnitř smyčky – obvykle await asyncio.sleep_ms(0) – aby ostatní připravené úlohy dostaly šanci běžet. Do tohoto tvaru patří i výpočetně náročná práce: smyčka zpracování obrazu, která běží stovky milisekund na iteraci, by měla alespoň jednou za iteraci předat řízení, aby se zbytek programu nezasekl.
8.15.3. Spolknutí CancelledError#
Stránka o rušení se tomuto již podrobně věnovala. Opakuje se to zde, protože je to nejčastější příčina problému „moje aplikace se nevypne“: korutina zachytí asyncio.CancelledError za účelem úklidu a zapomene ji znovu vyvolat. Úloha běží dál; volající, který si o zrušení požádal, navždy visí a čeká na její dokončení. Po úklidu vždy znovu vyvolejte výjimku, nebo místo explicitního except použijte blok try/finally.
8.15.5. await na úrovni modulu#
await je platné pouze uvnitř těla async def. Jeho zápis na úrovni modulu – mimo jakoukoli korutinu – je syntaktickou chybou:
# bug: not inside an async def
result = await fetch()
Řešením je vložit práci do korutiny a zavolat ji ze vstupního bodu programu asyncio.run().
8.15.6. Více volání asyncio.run#
MicroPython má jednu smyčku událostí. Volání asyncio.run() dvakrát za sebou – jednou pro nastavení, jednou pro hlavní práci – stále používá stejnou smyčku. Volání zevnitř běžící korutiny je chyba: smyčka už běží. Oba případy se nejčastěji objeví, když skript organicky roste a autor se jej snaží rozšířit přidáváním dalších volání run(), namísto aby novou práci začlenil do existující main.
8.15.7. Použití Event z přerušení#
asyncio.Event.set() je bezpečné volat pouze zevnitř smyčky událostí. Jeho volání z obslužné rutiny GPIO přerušení představuje riziko poškození. Pro probuzení úlohy z přerušení použijte místo toho ThreadSafeFlag – stránka o něm tento tvar pokrývá.
8.15.8. Dlouhá synchronní volání#
Korutina může čekat na vlastní čekací primitiva asyncio; cokoli jiného, co volá, běží synchronně a blokuje smyčku, dokud se to nevrátí. Blokující time.sleep() na 200 ms, zápis na SD kartu, jehož vyprázdnění trvá 80 ms, velká JPEG komprese, volání csi.CSI.snapshot() – každé z nich drží smyčku událostí po celou svou dobu trvání. Řešení závisí na volání:
Pro
time.sleep: nahraďte jejawait asyncio.sleepneboawait asyncio.sleep_ms.Pro
csi.CSI.snapshot: použijte asynchronní obal snapshotu, který buduje stránka o snímání.Pro dlouhé výpočty (zpracování obrazu, JPEG kódování): přijměte tu cenu nebo rozdělte práci na části, které mezi iteracemi
awaitují.
Asyncio nedokáže učinit synchronní volání neblokujícím. Dokáže pouze nechat běžet jiné korutiny zatímco se čeká na něco jiného.