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.