8.4. Gather

O capítulo anterior mostrou como asyncio.create_task() agenda uma corrotina e não espera por ela. A operação complementar – executar vários awaitables em simultâneo e esperar por todos eles – é asyncio.gather().

8.4.1. A forma básica

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

Saída:

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

Duas coisas a notar. Primeiro, a lista de resultados está pela ordem dos argumentos para o gather, não pela ordem em que as corrotinas terminaram – "c" retornou primeiro mas ainda é a terceira entrada. Segundo, a chamada demorou 200 ms no total, não 350 ms: os três sleeps correram em simultâneo, e gather() retorna assim que o mais lento terminar.

Ambos os factos têm a mesma origem. gather() envolve cada argumento que ainda não é uma tarefa (as corrotinas no exemplo), agenda-as no ciclo, suspende a corrotina chamante até que todas elas terminem, e depois devolve os seus resultados pela ordem original.

8.4.2. Quando recorrer a ele

Sempre que a aplicação tem N operações aguardáveis e quer os resultados de todas antes de continuar. Exemplos típicos:

  • Emitir vários pedidos de rede em paralelo e aguardar todas as respostas.

  • Ler de vários sensores em simultâneo antes de processar o resultado combinado.

  • Juntar várias tarefas auxiliares de curta duração no final de uma fase da aplicação.

Não é a ferramenta certa para tarefas de segundo plano de longa duração que a aplicação inicia e deixa correr durante todo o tempo de vida do programa – essas continuam a ser trabalho de create_task(). gather() é para o padrão de fan-out / fan-in: dividir o trabalho, fazê-lo em simultâneo, reunir.

8.4.3. Exceções no grupo

Se qualquer um dos awaitables recolhidos lançar uma exceção, o comportamento padrão é relançar a exceção para fora da chamada gather. Os irmãos que ainda não terminaram são cancelados em segundo plano.

Normalmente é isso que uma aplicação quer – um de N trabalhos paralelos falhou, portanto a operação combinada falhou, portanto pare de gastar tempo com o resto. Por vezes a aplicação quer o oposto: deixar cada awaitable terminar (ou falhar) de forma independente, e inspecionar os resultados depois. Para isso, passe return_exceptions=True

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

Cada entrada na lista devolvida é agora um valor de retorno normal ou a exceção que o awaitable correspondente lançou. O chamador verifica isinstance(r, Exception) para os distinguir.

8.4.4. Cancelamento

Cancelar o próprio gather – cancelando a tarefa que o estava a aguardar – cancela cada awaitable ainda em execução dentro dele. A página de timeouts e cancelamento cobre em detalhe como o cancelamento se propaga pela cadeia de chamadas.