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.