8.5. Timeouts och avbrytning

Avbrytning är asyncios namn för ”sluta köra den här korutinen, kasta ett undantag inuti den så att den får en chans att städa upp, och ta bort den från schemaläggaren”. Timeouts är den vanligaste anledningen att avbryta något; manuell task.cancel() är den andra.

8.5.1. Avbryta en uppgift

Att anropa Task.cancel schemalägger att asyncio.CancelledError ska kastas inuti den körande korutinen vid dess nästa await. Korutinen är fri att ignorera avbrytningen (fånga undantaget och fortsätta köra) eller att respektera den – det vanliga valet är att respektera den efter att städkod körts:

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

En naken finally-sats är det renaste mönstret: städningen körs oavsett om korutinen avslutades normalt, kastade ett orelaterat undantag eller avbröts. CancelledError propagerar tillbaka uppåt genom finally, loopen ser att uppgiften är klar, och anroparen av await task ser avbrytningen.

Att fånga CancelledError explicit är också fint när applikationen vill göra något specifikt med det – logga det, lämna över resursen på ett rent sätt, etc. Regeln är: kasta det på nytt efter att städningen körts. Att svälja CancelledError håller korutinen vid liv när dess anropare har bett den att stanna, vilket nästan alltid är en bugg:

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. Timeouts med wait_for

asyncio.wait_for() omsluter en väntbar med en deadline. Om den väntbara slutförs inom timeouten returneras dess resultat. Om inte avbryts den väntbara och anroparen får asyncio.TimeoutError

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

Sekundargumentet accepterar ett flyttal. För millisekundsformade deadlines tar asyncio.wait_for_ms() ett heltal av antalet millisekunder – en MicroPython-utökning som passar med den fasta programvarans millisekundsfina tidsreglage.

Internt gör wait_for exakt vad manuell avbrytning skulle göra: när deadlinen löper ut anropar den cancel() på den omslutna uppgiften, CancelledError kastas inuti korutinen, finally-satser körs, och när städningen är klar översätts undantaget till en TimeoutError för anroparen.

Det betyder att en korutin som fångar och ignorerar CancelledError kommer att omintetgöra timeouten – deadlinen löpte ut, men korutinen vägrade stanna, och wait_for kan inte tvinga den. Den tidigare regeln gäller här också: fånga CancelledError enbart för att köra städning, och kasta det sedan på nytt.

8.5.3. Avbrytning genom gather

Avbrytning propagerar nedåt genom gather(). Om den uppgift som väntar på ett gather-anrop avbryts avbryts varje väntbar som fortfarande körs inuti gather också – var och en får en chans att städa upp genom sina egna finally-satser innan avbrytningen rullar upp till anroparen.

Kombinerat med timeouts är detta standardsättet att sätta en deadline på en grupp operationer:

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

Antingen slutförs varje deloperation inom fem sekunder, eller så avbryts alla tillsammans.

8.5.4. Stänga ner en applikation

Avbrytning är också hur en riktig applikation stannar på ett rent sätt. Mönstret är konsekvent över skript: main fångar handtagen för de långlivade bakgrundsuppgifter den startade, kör sitt arbete på toppnivå, avbryter sedan varje handtag och väntar på det i ett finally-block:

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 är knepet som hindrar gather från att kasta om CancelledError som varje barnuppgift är på väg att leverera, så att applikationens egen avslutsorsak – vad snapshot_loop än gjorde eller inte kastade – är det som bubblar ut ur main.