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.