8.4. Gather¶
Le chapitre précédent a montré comment asyncio.create_task() planifie une coroutine et n’attend pas qu’elle se termine. L’opération complémentaire – exécuter plusieurs awaitables simultanément et attendre qu’ils se terminent tous – est asyncio.gather().
8.4.1. La structure de base¶
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())
Sortie
['a', 'b', 'c']
Deux choses à remarquer. Premièrement, la liste de résultats est dans l’ordre des arguments passés à gather, et non dans l’ordre dans lequel les coroutines se sont terminées – "c" a renvoyé en premier mais reste néanmoins la troisième entrée. Deuxièmement, l’appel a pris 200 ms au total, et non 350 ms : les trois mises en sommeil se sont exécutées simultanément, et gather() renvoie dès que la plus lente est terminée.
Ces deux faits proviennent de la même source. gather() enveloppe chaque argument qui n’est pas déjà une tâche (les coroutines dans l’exemple), les planifie sur la boucle, suspend la coroutine appelante jusqu’à ce que toutes soient terminées, puis renvoie leurs résultats dans l’ordre d’origine.
8.4.2. Quand y avoir recours¶
Partout où l’application a N opérations awaitables et souhaite obtenir les résultats de toutes avant de continuer. Exemples typiques :
Émettre plusieurs requêtes réseau en parallèle et attendre toutes les réponses.
Lire plusieurs capteurs simultanément avant de traiter le résultat combiné.
Réunir plusieurs tâches auxiliaires de courte durée à la fin d’une phase de l’application.
Ce n’est pas le bon outil pour les tâches d’arrière-plan de longue durée que l’application démarre et laisse tourner pendant toute la vie du programme – celles-ci relèvent encore de create_task(). gather() sert au motif fan-out / fan-in : répartir le travail, l’effectuer simultanément, puis le réunir.
8.4.3. Exceptions dans le groupe¶
Si l’un des awaitables rassemblés lève une exception, le comportement par défaut consiste à relever l’exception hors de l’appel à gather. Les frères et sœurs qui ne sont pas encore terminés sont annulés en arrière-plan.
C’est généralement ce que veut une application – l’un des N travaux parallèles a échoué, donc l’opération combinée a échoué, donc cessons de passer du temps sur le reste. Parfois, l’application veut l’inverse : laisser chaque awaitable se terminer (ou échouer) indépendamment, puis inspecter les résultats ensuite. Pour cela, passez return_exceptions=True
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
Chaque entrée de la liste renvoyée est désormais soit une valeur de retour normale, soit l’exception levée par l’awaitable correspondant. L’appelant vérifie isinstance(r, Exception) pour les distinguer.
8.4.4. Annulation¶
Annuler le gather lui-même – en annulant la tâche qui l’attendait – annule tous les awaitables encore en cours d’exécution à l’intérieur. La page délais et annulation couvre en détail la manière dont l’annulation se propage à travers la chaîne d’appels.