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ń.