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.