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.