8.5. Aikakatkaisut ja peruminen#

Peruminen on asyncion nimitys toiminnolle ”lopeta tämän korutiinin suoritus, nosta sen sisällä poikkeus jotta se saa mahdollisuuden siivota, ja poista se ajastimesta”. Aikakatkaisut ovat yleisin syy perua jokin; manuaalinen task.cancel() on toinen.

8.5.1. Tehtävän peruminen#

Task.cancel-kutsu ajastaa asyncio.CancelledError-poikkeuksen nostettavaksi käynnissä olevan korutiinin sisällä sen seuraavassa await-kohdassa. Korutiini voi vapaasti jättää perumisen huomiotta (napata poikkeus ja jatkaa suoritusta) tai noudattaa sitä – tavallinen valinta on noudattaa sitä siivouskoodin suorittamisen jälkeen:

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

Paljas finally-lause on siisteimpiä malleja: siivous suoritetaan riippumatta siitä, päättyikö korutiini normaalisti, nostiko se asiaankuulumattoman poikkeuksen vai peruttiinko se. CancelledError etenee takaisin ylöspäin finally-lauseen läpi, silmukka näkee tehtävän valmistuneen, ja await task-kutsun kutsuja näkee perumisen.

CancelledError-poikkeuksen nappaaminen eksplisiittisesti on myös hyväksyttävää, kun sovellus haluaa tehdä sillä jotain erityistä – kirjata sen, luovuttaa resurssin siististi jne. Sääntö on: nosta se uudelleen siivouksen suorittamisen jälkeen. CancelledError-poikkeuksen nieleminen pitää korutiinin elossa, kun sen kutsuja on pyytänyt sitä pysähtymään, mikä on lähes aina bugi:

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. Aikakatkaisut wait_for-funktiolla#

asyncio.wait_for() käärii odotettavan määräaikaan. Jos odotettava päättyy aikakatkaisun sisällä, sen tulos palautetaan. Jos se ei päätä, odotettava perutaan ja kutsuja saa asyncio.TimeoutError-poikkeuksen:

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

Sekuntiargumentti hyväksyy liukuluvun. Millisekuntimuotoisia määräaikoja varten asyncio.wait_for_ms() ottaa kokonaislukuisen millisekuntimäärän – MicroPython-laajennus, joka sopii yhteen laiteohjelmiston millisekuntitarkkuisten ajastussäätimien kanssa.

Sisäisesti wait_for tekee täsmälleen sen, mitä manuaalinen peruminen tekisi: kun määräaika umpeutuu, se kutsuu cancel() kääritylle tehtävälle, CancelledError nostetaan korutiinin sisällä, finally-lauseet suoritetaan, ja kun siivous on tehty, poikkeus käännetään TimeoutError-poikkeukseksi kutsujalle.

Tämä tarkoittaa, että korutiini, joka nappaa ja jättää huomiotta CancelledError-poikkeuksen, kumoaa aikakatkaisun – määräaika umpeutui, mutta korutiini kieltäytyi pysähtymästä, eikä wait_for voi pakottaa sitä. Aiempi sääntö pätee tässäkin: nappaa CancelledError vain siivouksen suorittamiseksi, ja nosta se sitten uudelleen.

8.5.3. Peruminen gatherin kautta#

Peruminen etenee alaspäin gather()-funktion läpi. Jos tehtävä, joka odottaa gather-kutsua, perutaan, jokainen gatherin sisällä yhä käynnissä oleva odotettava perutaan myös – jokainen saa mahdollisuuden siivota omien finally-lauseidensa kautta ennen kuin peruminen kiertyy ylös kutsujalle.

Yhdistettynä aikakatkaisuihin tämä on vakiotapa asettaa määräaika ryhmälle operaatioita:

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

Joko jokainen alioperaatio päättyy viiden sekunnin sisällä, tai ne kaikki perutaan yhdessä.

8.5.4. Sovelluksen sammuttaminen#

Peruminen on myös tapa, jolla oikea sovellus pysähtyy siististi. Malli on yhdenmukainen eri skripteissä: main tallentaa kahvat käynnistämilleen pitkäikäisille taustatehtäville, suorittaa ylätason työnsä, ja perii sitten jokaisen kahvan ja odottaa sitä finally-lohkossa:

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 on temppu, joka estää gatheria nostamasta uudelleen CancelledError-poikkeusta, jonka jokainen lapsitehtävä on toimittamaisillaan, jotta sovelluksen oma lopetussyy – mitä tahansa snapshot_loop teki tai jätti nostamatta – on se, mikä kuplii ulos main-funktiosta.