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.