8.5. Timeouts und Abbruch¶
Abbruch ist asyncios Bezeichnung für „diese Koroutine anhalten, eine Ausnahme darin auslösen, damit sie die Gelegenheit zum Aufräumen erhält, und sie aus dem Scheduler entfernen“. Timeouts sind der häufigste Grund, etwas abzubrechen; ein manuelles task.cancel() ist der andere.
8.5.1. Eine Aufgabe abbrechen¶
Der Aufruf von Task.cancel plant, dass asyncio.CancelledError innerhalb der laufenden Koroutine bei ihrem nächsten await ausgelöst wird. Es steht der Koroutine frei, den Abbruch zu ignorieren (die Ausnahme abfangen und weiterlaufen) oder ihm Folge zu leisten – die übliche Wahl ist, ihm nach dem Ausführen von Aufräumcode Folge zu leisten:
async def network_worker(stream):
try:
while True:
data = await stream.read(64)
# ...
finally:
stream.close()
Eine schlichte finally-Klausel ist das sauberste Muster: Das Aufräumen läuft, egal ob die Koroutine normal beendet wurde, eine nicht zusammenhängende Ausnahme ausgelöst hat oder abgebrochen wurde. Der CancelledError pflanzt sich durch das finally zurück hinauf, die Schleife sieht, dass die Aufgabe fertig ist, und der Aufrufer von await task sieht den Abbruch.
CancelledError explizit abzufangen ist ebenfalls in Ordnung, wenn die Anwendung etwas Bestimmtes damit tun möchte – ihn protokollieren, die Ressource sauber übergeben usw. Die Regel lautet: ihn erneut auslösen, nachdem das Aufräumen gelaufen ist. Den CancelledError zu schlucken, hält die Koroutine am Leben, wenn ihr Aufrufer sie zum Anhalten aufgefordert hat, was fast immer ein Fehler ist:
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 mit wait_for¶
asyncio.wait_for() umschließt ein Awaitable mit einer Frist. Wenn das Awaitable innerhalb des Timeouts fertig wird, wird sein Ergebnis zurückgegeben. Falls nicht, wird das Awaitable abgebrochen und der Aufrufer erhält asyncio.TimeoutError:
try:
frame = await asyncio.wait_for(grab_frame(), timeout=2)
except asyncio.TimeoutError:
print("camera took too long")
Das Sekundenargument akzeptiert einen Float. Für millisekundengeformte Fristen nimmt asyncio.wait_for_ms() eine ganzzahlige Millisekundenanzahl entgegen – eine MicroPython-Erweiterung, die zu den millisekundengranularen Zeit-Stellschrauben der Firmware passt.
Intern tut wait_for genau das, was ein manueller Abbruch tun würde: Wenn die Frist abläuft, ruft es cancel() auf der umschlossenen Aufgabe auf, CancelledError wird innerhalb der Koroutine ausgelöst, finally-Klauseln laufen, und sobald das Aufräumen erledigt ist, wird die Ausnahme für den Aufrufer in einen TimeoutError übersetzt.
Das bedeutet, dass eine Koroutine, die CancelledError abfängt und ignoriert, den Timeout aushebelt – die Frist ist abgelaufen, aber die Koroutine hat sich geweigert anzuhalten, und wait_for kann sie nicht dazu zwingen. Die frühere Regel gilt auch hier: CancelledError nur abfangen, um aufzuräumen, und dann erneut auslösen.
8.5.3. Abbruch über gather¶
Ein Abbruch pflanzt sich über gather() nach unten fort. Wenn die Aufgabe, die auf einen gather-Aufruf wartet, abgebrochen wird, wird jedes Awaitable, das noch innerhalb des gather läuft, ebenfalls abgebrochen – jedes erhält über seine eigenen finally-Klauseln die Gelegenheit zum Aufräumen, bevor sich der Abbruch zum Aufrufer hinaufrollt.
In Kombination mit Timeouts ist dies die übliche Methode, einer Gruppe von Operationen eine Frist zu setzen:
await asyncio.wait_for(
asyncio.gather(a(), b(), c()),
timeout=5,
)
Entweder werden alle Teiloperationen innerhalb von fünf Sekunden fertig, oder alle werden gemeinsam abgebrochen.
8.5.4. Eine Anwendung herunterfahren¶
Ein Abbruch ist auch die Art und Weise, wie eine echte Anwendung sauber stoppt. Das Muster ist über Skripte hinweg konsistent: main erfasst die Handles für die langlebigen Hintergrundaufgaben, die es gestartet hat, führt seine Arbeit auf oberster Ebene aus und bricht dann jedes Handle ab und wartet es in einem finally-Block ab:
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)
Das return_exceptions=True ist der Kniff, der den gather davon abhält, den CancelledError, den jede Kindaufgabe gleich liefern wird, erneut auszulösen, sodass der eigene Beendigungsgrund der Anwendung – was auch immer snapshot_loop ausgelöst hat oder nicht – das ist, was aus main hervorsprudelt.