8.4. Gather¶
Das vorige Kapitel hat gezeigt, wie asyncio.create_task() eine Koroutine plant und nicht auf sie wartet. Die ergänzende Operation – mehrere Awaitables nebenläufig ausführen und auf alle von ihnen warten – ist asyncio.gather().
8.4.1. Die grundlegende Form¶
import asyncio
async def fetch(name, delay_ms):
await asyncio.sleep_ms(delay_ms)
return name
async def main():
results = await asyncio.gather(
fetch("a", 100),
fetch("b", 200),
fetch("c", 50),
)
print(results)
asyncio.run(main())
Ausgabe:
['a', 'b', 'c']
Zwei Dinge sind zu beachten. Erstens ist die Ergebnisliste in der Reihenfolge der Argumente von gather, nicht in der Reihenfolge, in der die Koroutinen fertig wurden – "c" wurde zuerst zurückgegeben, ist aber dennoch der dritte Eintrag. Zweitens dauerte der Aufruf insgesamt 200 ms, nicht 350 ms: Die drei Schlafphasen liefen nebenläufig, und gather() kehrt zurück, sobald die langsamste abgeschlossen ist.
Beide Tatsachen entspringen derselben Quelle. gather() umschließt jedes Argument, das nicht bereits eine Aufgabe ist (die Koroutinen im Beispiel), plant sie auf der Schleife, hält die aufrufende Koroutine an, bis alle von ihnen fertig sind, und gibt dann ihre Ergebnisse in der ursprünglichen Reihenfolge zurück.
8.4.2. Wann man dazu greift¶
Überall dort, wo die Anwendung N awaitable Operationen hat und die Ergebnisse aller von ihnen haben möchte, bevor sie fortfährt. Typische Beispiele:
Mehrere Netzwerkanfragen parallel ausgeben und auf alle Antworten warten.
Von mehreren Sensoren nebenläufig lesen, bevor das kombinierte Ergebnis verarbeitet wird.
Mehrere kurzlebige Hilfsaufgaben am Ende einer Anwendungsphase zusammenführen.
Es ist nicht das richtige Werkzeug für langlaufende Hintergrundaufgaben, die die Anwendung startet und für die Lebensdauer des Programms laufen lässt – das ist weiterhin Arbeit für create_task(). gather() ist für das Fan-out / Fan-in-Muster gedacht: Arbeit aufteilen, nebenläufig erledigen, wieder zusammenführen.
8.4.3. Ausnahmen in der Gruppe¶
Wenn eines der gesammelten Awaitables eine Ausnahme auslöst, besteht das Standardverhalten darin, die Ausnahme aus dem gather-Aufruf erneut auszulösen. Die Geschwister, die noch nicht fertig sind, werden im Hintergrund abgebrochen.
Das ist meist genau das, was eine Anwendung möchte – einer von N parallelen Jobs ist fehlgeschlagen, also ist die kombinierte Operation fehlgeschlagen, also sollte man keine Zeit mehr mit dem Rest verbringen. Manchmal möchte die Anwendung das Gegenteil: jedes Awaitable unabhängig fertig werden (oder fehlschlagen) lassen und die Ergebnisse anschließend untersuchen. Dafür übergibt man return_exceptions=True:
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
Jeder Eintrag in der zurückgegebenen Liste ist nun entweder ein normaler Rückgabewert oder die Ausnahme, die das entsprechende Awaitable ausgelöst hat. Der Aufrufer prüft isinstance(r, Exception), um sie zu unterscheiden.
8.4.4. Abbruch¶
Den gather selbst abzubrechen – indem man die Aufgabe abbricht, die auf ihn wartet – bricht jedes Awaitable ab, das noch darin läuft. Die Seite Timeouts und Abbruch behandelt im Detail, wie sich ein Abbruch durch die Aufrufkette fortpflanzt.