8.4. Gather

Poprzedni rozdział pokazał, jak asyncio.create_task() planuje korutynę i nie czeka na nią. Operacją towarzyszącą – uruchom kilka obiektów awaitable współbieżnie i poczekaj na wszystkie z nich – jest asyncio.gather().

8.4.1. Podstawowa postać

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())

Wynik:

['a', 'b', 'c']

Dwie rzeczy warte uwagi. Po pierwsze, lista wyników jest w kolejności argumentów przekazanych do gather, a nie w kolejności, w jakiej korutyny się zakończyły – "c" zwróciło wartość jako pierwsze, ale nadal jest trzecim wpisem. Po drugie, wywołanie zajęło łącznie 200 ms, a nie 350 ms: trzy uśpienia działały współbieżnie, a gather() zwraca, gdy tylko zakończy się najwolniejsze z nich.

Oba fakty pochodzą z tego samego źródła. gather() opakowuje każdy argument, który nie jest już zadaniem (korutyny w przykładzie), planuje je w pętli, zawiesza wywołującą korutynę aż do zakończenia wszystkich z nich, a następnie zwraca ich wyniki w oryginalnej kolejności.

8.4.2. Kiedy po nie sięgać

Wszędzie tam, gdzie aplikacja ma N operacji typu awaitable i chce uzyskać wyniki ich wszystkich przed kontynuowaniem. Typowe przykłady:

  • Wysyłanie kilku żądań sieciowych równolegle i oczekiwanie na wszystkie odpowiedzi.

  • Odczyt z kilku sensorów współbieżnie przed przetworzeniem połączonego wyniku.

  • Łączenie kilku krótkotrwałych zadań pomocniczych na końcu fazy aplikacji.

Nie jest to właściwe narzędzie do długotrwałych zadań w tle, które aplikacja uruchamia i pozostawia działające przez cały czas życia programu – to nadal jest praca dla create_task(). gather() służy do wzorca fan-out / fan-in: podziel pracę, wykonaj ją współbieżnie, połącz ponownie.

8.4.3. Wyjątki w grupie

Jeśli którykolwiek z zebranych obiektów awaitable zgłosi wyjątek, domyślnym zachowaniem jest ponowne zgłoszenie wyjątku na zewnątrz wywołania gather. Pozostałe, które jeszcze się nie zakończyły, są anulowane w tle.

Zwykle jest to dokładnie to, czego chce aplikacja – jedno z N równoległych zadań zawiodło, więc połączona operacja zawiodła, więc przestań tracić czas na resztę. Czasami aplikacja chce czegoś przeciwnego: pozwolić każdemu obiektowi awaitable zakończyć się (lub zawieść) niezależnie, a następnie przeanalizować wyniki. W tym celu przekaż return_exceptions=True

results = await asyncio.gather(
    fetch_or_fail("ok"),
    fetch_or_fail("bad"),
    return_exceptions=True,
)
# results == ["ok-value", OSError(...)]

Każdy wpis na zwracanej liście jest teraz albo normalną wartością zwracaną, albo wyjątkiem zgłoszonym przez odpowiadający mu obiekt awaitable. Wywołujący sprawdza isinstance(r, Exception), aby je rozróżnić.

8.4.4. Anulowanie

Anulowanie samego gather – poprzez anulowanie zadania, które na niego oczekiwało – anuluje każdy obiekt awaitable nadal działający w jego wnętrzu. Strona limity czasu i anulowanie szczegółowo omawia, jak anulowanie propaguje się przez łańcuch wywołań.