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.