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.4. Mutace sdíleného stavu napříč await body

Kooperativní plánování zaručuje, že korutina má CPU sama pro sebe mezi await body, ale jakmile awaituje, může běžet jiná korutina. Pokud dvě korutiny upravují stejnou datovou strukturu v krocích, které zahrnují await, mohou se jejich operace prolnout způsoby, které strukturu poškodí:

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

U stavu, který je mutován pouze mezi await body uvnitř jedné korutiny, není potřeba žádná synchronizace. U stavu mutovaného napříč await body a přistupovaného z více než jedné korutiny obalte kritickou sekci pomocí Lock.

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 ThreadSafeFlagstrá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 jej await asyncio.sleep nebo await 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.