8.5. Timeouts en annulering¶
Annulering is asyncio’s naam voor “stop met het draaien van deze coroutine, werp er een exceptie in op zodat het de kans krijgt om op te schonen, en verwijder het uit de scheduler”. Timeouts zijn de meest voorkomende reden om iets te annuleren; handmatige task.cancel() is de andere.
8.5.1. Een taak annuleren¶
Het aanroepen van Task.cancel plant in dat asyncio.CancelledError binnen de draaiende coroutine wordt opgeworpen bij zijn volgende await. De coroutine is vrij om de annulering te negeren (de exceptie opvangen en blijven draaien) of er gehoor aan te geven – de gebruikelijke keuze is om er gehoor aan te geven nadat de opschooncode is gedraaid:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Een kale finally-clausule is het schoonste patroon: de opschoning draait of de coroutine nu normaal afsloot, een ongerelateerde exceptie opwierp of geannuleerd werd. De CancelledError propageert terug omhoog door de finally, de loop ziet dat de taak klaar is, en de aanroeper van await task ziet de annulering.
Het expliciet opvangen van CancelledError is ook prima wanneer de applicatie er iets specifieks mee wil doen – het loggen, de resource netjes overdragen, enz. De regel is: werp het opnieuw op nadat de opschoning is gedraaid. Het inslikken van CancelledError houdt de coroutine in leven terwijl zijn aanroeper het heeft gevraagd te stoppen, wat bijna altijd een bug is:
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. Timeouts met wait_for¶
asyncio.wait_for() wikkelt een awaitable in een deadline. Als de awaitable binnen de timeout klaar is, wordt het resultaat geretourneerd. Zo niet, dan wordt de awaitable geannuleerd en krijgt de aanroeper een asyncio.TimeoutError
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Het secondenargument accepteert een float. Voor deadlines in milliseconden neemt asyncio.wait_for_ms() een geheel aantal milliseconden – een MicroPython-uitbreiding die aansluit bij de milliseconde-grovige timinginstellingen van de firmware.
Intern doet wait_for precies wat handmatige annulering zou doen: wanneer de deadline verloopt roept het cancel() aan op de ingewikkelde taak, wordt CancelledError binnen de coroutine opgeworpen, draaien de finally-clausules, en zodra de opschoning klaar is wordt de exceptie vertaald naar een TimeoutError voor de aanroeper.
Dat betekent dat een coroutine die CancelledError opvangt en negeert de timeout zal verslaan – de deadline verliep, maar de coroutine weigerde te stoppen, en wait_for kan het niet forceren. De eerdere regel geldt ook hier: vang CancelledError alleen op om op te schonen, en werp het dan opnieuw op.
8.5.3. Annulering via gather¶
Annulering propageert neerwaarts door gather(). Als de taak die op een gather-aanroep wacht wordt geannuleerd, wordt elke awaitable die nog binnen de gather draait ook geannuleerd – elk krijgt de kans om op te schonen via zijn eigen finally-clausules voordat de annulering oprolt naar de aanroeper.
Gecombineerd met timeouts is dit de standaardmanier om een deadline op een groep bewerkingen te zetten:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Ofwel elke deelbewerking eindigt binnen vijf seconden, of ze worden allemaal samen geannuleerd.
8.5.4. Een applicatie afsluiten¶
Annulering is ook hoe een echte applicatie netjes stopt. Het patroon is consistent over scripts heen: main legt de handles vast voor de langlevende achtergrondtaken die het is gestart, draait zijn werk op het hoogste niveau, en annuleert dan elke handle en wacht erop in een finally-blok:
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)
De return_exceptions=True is de truc die voorkomt dat de gather de CancelledError opnieuw opwerpt die elke kindtaak op het punt staat te leveren, zodat de eigen afsluitreden van de applicatie – wat snapshot_loop wel of niet opwierp – is wat naar boven borrelt uit main.