8.5. Istek vremena i otkazivanje¶
Otkazivanje je asyncio naziv za „prestani izvršavati ovu korutinu, izazovi iznimku unutar nje kako bi imala priliku počistiti za sobom i ukloni je iz rasporeda”. Istek vremena najčešći je razlog za otkazivanje nečega; ručno task.cancel() je drugi.
8.5.1. Otkazivanje zadatka¶
Poziv Task.cancel raspoređuje asyncio.CancelledError za izazivanje unutar korutine koja se izvršava na njezinu sljedećem await. Korutina slobodno može zanemariti otkazivanje (uhvatiti iznimku i nastaviti s radom) ili ga uvažiti – uobičajeni izbor je uvažiti ga nakon izvršavanja koda za čišćenje:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Gola klauzula finally najčišći je obrazac: čišćenje se izvršava bilo da je korutina izašla normalno, izazvala nepovezanu iznimku ili je otkazana. CancelledError propagira se natrag kroz finally, petlja vidi da je zadatak gotov, a pozivatelj await task vidi otkazivanje.
Eksplicitno hvatanje CancelledError također je u redu kad aplikacija želi s njim učiniti nešto specifično – zabilježiti ga, čisto predati resurs itd. Pravilo je: ponovno ga izazovi nakon što se čišćenje izvrši. Progutanje CancelledError održava korutinu na životu nakon što ju je njezin pozivatelj zatražio da stane, što je gotovo uvijek greška:
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. Istek vremena s wait_for¶
asyncio.wait_for() omata awaitable u rok. Ako awaitable završi unutar isteka vremena, vraća se njegov rezultat. Ako ne završi, awaitable se otkazuje, a pozivatelj dobiva asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Argument za sekunde prihvaća decimalni broj. Za rokove u obliku milisekundi asyncio.wait_for_ms() uzima cjelobrojni broj milisekundi – proširenje MicroPythona koje se poklapa s postavkama mjerenja vremena ugrađenog programa (firmware) granuliranim na milisekunde.
Interno, wait_for radi točno ono što bi radilo ručno otkazivanje: kad rok istekne, poziva cancel() na omotanom zadatku, CancelledError se izaziva unutar korutine, klauzule finally se izvršavaju, a nakon što je čišćenje gotovo, iznimka se prevodi u TimeoutError za pozivatelja.
To znači da će korutina koja uhvati i zanemari CancelledError poništiti istek vremena – rok je istekao, ali korutina je odbila stati, a wait_for je ne može prisiliti. Ovdje vrijedi i ranije pravilo: uhvati CancelledError samo da bi izvršio čišćenje, a zatim ga ponovno izazovi.
8.5.3. Otkazivanje kroz gather¶
Otkazivanje se propagira prema dolje kroz gather(). Ako se otkaže zadatak koji čeka na poziv gathera, otkazuje se i svaki awaitable koji se još uvijek izvršava unutar gathera – svaki dobiva priliku za čišćenje kroz vlastite klauzule finally prije nego što se otkazivanje uspne natrag do pozivatelja.
Kombinirano s istekom vremena, ovo je standardan način za postavljanje roka na grupu operacija:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Ili svaka podoperacija završi unutar pet sekundi, ili se sve one otkažu zajedno.
8.5.4. Gašenje aplikacije¶
Otkazivanje je također način na koji se prava aplikacija čisto zaustavlja. Obrazac je dosljedan u svim skriptama: main hvata ručke za dugotrajne pozadinske zadatke koje je pokrenuo, izvršava svoj posao najviše razine, zatim otkazuje svaku ručku i čeka na nju u 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 koji sprječava gather da ponovno izazove CancelledError koji svaki podređeni zadatak upravo namjerava isporučiti, tako da je vlastiti razlog izlaska aplikacije – što god snapshot_loop izazvao ili ne izazvao – ono što izbija iz main.