8.5. Timeouty a rušení#

Rušení je název, který asyncio dává významu „zastav běh této korutiny, vyvolej v ní výjimku, aby měla šanci uklidit, a odeber ji z plánovače“. Timeouty jsou nejčastějším důvodem k něčímu zrušení; ručně volané task.cancel() je tím druhým.

8.5.1. Rušení úlohy#

Volání Task.cancel naplánuje vyvolání asyncio.CancelledError uvnitř běžící korutiny u jejího příštího await. Korutina může rušení ignorovat (zachytit výjimku a běžet dál) nebo jej respektovat – obvyklou volbou je respektovat jej po spuštění čistícího kódu:

async def network_worker(stream):
    try:
        while True:
            data = await stream.read(64)
            # ...
    finally:
        stream.close()

Holá klauzule finally je nejčistším vzorem: úklid proběhne, ať už korutina skončila normálně, vyvolala nesouvisející výjimku, nebo byla zrušena. CancelledError se propaguje zpět nahoru přes finally, smyčka vidí, že úloha je hotová, a volající await task vidí zrušení.

Explicitní zachycení CancelledError je také v pořádku, když s ním aplikace chce udělat něco konkrétního – zaznamenat jej, čistě předat prostředek atd. Pravidlo zní: po proběhnutí úklidu jej znovu vyvolejte. Spolknutí CancelledError udrží korutinu naživu, když ji její volající požádal o zastavení, což je téměř vždy chyba:

async def worker():
    try:
        await long_running_thing()
    except asyncio.CancelledError:
        log("worker cancelled, cleaning up")
        close_resources()
        raise          # << this line is the important one

8.5.2. Timeouty s wait_for#

asyncio.wait_for() obalí awaitable objekt lhůtou. Pokud awaitable objekt skončí v rámci timeoutu, vrátí se jeho výsledek. Pokud ne, awaitable objekt je zrušen a volající dostane asyncio.TimeoutError

try:
    frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
    print("camera took too long")

Argument se sekundami přijímá číslo s plovoucí desetinnou čárkou. Pro lhůty v milisekundách přijímá asyncio.wait_for_ms() celočíselný počet milisekund – rozšíření MicroPythonu, které ladí s milisekundově odstupňovanými časovacími prvky firmwaru.

Interně wait_for dělá přesně to, co by udělalo ruční rušení: když lhůta vyprší, zavolá cancel() na obalené úloze, uvnitř korutiny se vyvolá CancelledError, proběhnou klauzule finally, a jakmile je úklid hotov, výjimka se pro volajícího přeloží na TimeoutError.

To znamená, že korutina, která zachytí a ignoruje CancelledError, zmaří timeout – lhůta vypršela, ale korutina se odmítla zastavit a wait_for ji k tomu nemůže donutit. I zde platí dřívější pravidlo: zachyťte CancelledError jen kvůli spuštění úklidu a poté ji znovu vyvolejte.

8.5.3. Rušení skrze gather#

Rušení se propaguje dolů skrze gather(). Pokud je zrušena úloha, která čeká na volání gather, je zrušen i každý awaitable objekt, který v gather stále běží – každý z nich dostane šanci uklidit přes vlastní klauzule finally, než se rušení převalí nahoru k volajícímu.

V kombinaci s timeouty je toto standardní způsob, jak nastavit lhůtu na skupinu operací:

await asyncio.wait_for(
    asyncio.gather(a(), b(), c()),
    timeout=5,
)

Buď každá dílčí operace skončí do pěti sekund, nebo jsou všechny zrušeny společně.

8.5.4. Vypínání aplikace#

Rušení je také způsob, jakým se reálná aplikace čistě zastaví. Tento vzor je napříč skripty konzistentní: main zachytí handly dlouho žijících úloh na pozadí, které spustila, vykoná svou práci nejvyšší úrovně, poté každý handle zruší a počká na něj v bloku finally

async def main():
    sender = asyncio.create_task(uplink())
    watcher = asyncio.create_task(button_watcher())
    try:
        await snapshot_loop()
    finally:
        sender.cancel()
        watcher.cancel()
        await asyncio.gather(sender, watcher,
                             return_exceptions=True)

return_exceptions=True je trik, který zabrání tomu, aby gather znovu vyvolal CancelledError, kterou se chystá doručit každá podřízená úloha, takže vlastní důvod ukončení aplikace – ať už snapshot_loop vyvolal cokoli, nebo nic – je to, co vybublá ven z main.