8.5. Időtúllépések és megszakítás

A megszakítás az asyncio neve arra, hogy „állítsd le ennek a korutinnak a futását, válts ki benne egy kivételt, hogy esélye legyen a tisztításra, és távolítsd el az ütemezőből”. Az időtúllépések a leggyakoribb ok valaminek a megszakítására; a kézi task.cancel() a másik.

8.5.1. Egy feladat megszakítása

A Task.cancel hívása beütemezi, hogy az asyncio.CancelledError a futó korutinban a következő await helyén kiváltódjon. A korutin szabadon figyelmen kívül hagyhatja a megszakítást (elkapja a kivételt és tovább fut), vagy tiszteletben tarthatja azt – a szokásos választás az, hogy a tisztítási kód lefuttatása után tiszteletben tartja:

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

Egy önálló finally ág a legtisztább minta: a tisztítás lefut, akár normálisan lépett ki a korutin, akár egy nem kapcsolódó kivételt váltott ki, akár megszakították. A CancelledError visszaterjed a finally ágon keresztül, a hurok látja, hogy a feladat kész, és az await task hívója látja a megszakítást.

Az CancelledError explicit elkapása is rendben van, amikor az alkalmazás valami konkrétat akar vele tenni – naplózni, az erőforrást tisztán átadni stb. A szabály a következő: futtasd a tisztítást, majd váltsd ki újra. A CancelledError elnyelése életben tartja a korutint, amikor a hívója kérte a leállását, ami szinte mindig hiba:

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. Időtúllépések a wait_for hívással

Az asyncio.wait_for() egy várhatót egy határidőbe burkol. Ha a várható az időtúllépésen belül befejeződik, az eredménye visszaadódik. Ha nem, a várható megszakad, és a hívó asyncio.TimeoutError kivételt kap:

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

A másodperc argumentum lebegőpontos értéket fogad el. Ezredmásodperc-jellegű határidőkhöz az asyncio.wait_for_ms() egy egész ezredmásodperc-számot vesz át – ez egy MicroPython-bővítmény, amely illeszkedik a firmware ezredmásodperc-finomságú időzítési beállításaihoz.

Belül a wait_for pontosan azt teszi, amit a kézi megszakítás tenne: amikor a határidő lejár, meghívja a cancel() hívást a beburkolt feladaton, az CancelledError kiváltódik a korutinban, a finally ágak lefutnak, és amint a tisztítás kész, a kivétel a hívó számára TimeoutError kivétellé fordítódik le.

Ez azt jelenti, hogy egy korutin, amely elkapja és figyelmen kívül hagyja az CancelledError kivételt, meghiúsítja az időtúllépést – a határidő lejárt, de a korutin nem volt hajlandó leállni, és a wait_for nem tudja kényszeríteni. A korábbi szabály itt is érvényes: az CancelledError kivételt csak a tisztítás futtatására kapd el, majd váltsd ki újra.

8.5.3. Megszakítás a gatheren keresztül

A megszakítás lefelé terjed az gather() hívásán keresztül. Ha az a feladat, amely megvár egy gather hívást, megszakad, akkor a gatheren belül még futó minden várható is megszakad – mindegyik kap egy esélyt, hogy a saját finally ágain keresztül tisztítson, mielőtt a megszakítás felgördül a hívóig.

Időtúllépésekkel kombinálva ez a szokásos módja annak, hogy határidőt szabjunk egy csoportnyi műveletre:

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

Vagy minden részművelet befejeződik öt másodpercen belül, vagy mindegyik együtt megszakad.

8.5.4. Egy alkalmazás leállítása

A megszakítás az is, ahogy egy valódi alkalmazás tisztán leáll. A minta a szkripteken keresztül következetes: a main rögzíti az általa elindított hosszú életű háttérfeladatok kezelőit, lefuttatja a legfelső szintű munkáját, majd egy finally blokkban megszakítja minden kezelőt és megvárja:

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)

A return_exceptions=True az a trükk, amely megakadályozza, hogy a gather újra kiváltsa az CancelledError kivételt, amelyet minden gyermekfeladat éppen szállítani készül, így az alkalmazás saját kilépési oka – bármit is váltott ki vagy nem váltott ki a snapshot_loop – az, ami a main hívásból felbukkan.