1
resposta

Processamento do asyncio.gather(...)

Olá!

Contexto

Abaixo segue a minha solução para o exercício 07 (que pode ser vista aqui também). A única diferença significativa, ao meu ver, é o uso do método task.done() o qual eu não conhecia.

A priori, tive dificuldade em entender quando eu faria a chamada asyncio.gather(...) e estava insistindo em fazê-la antes do laço de repetição. Obviamente o programa não executava como esperado, finalizando sem verificar o status das tarefas (ou melhor, as tarefas sempre que eram verificadas - apenas 1 vez - já estavam com o status de concluída).
Na minha interpretação inicial a chamada asyncio.gather(...) era responsável por executar efetivamente as tasks e colocá-la após o loop não faria sentido pois o programa ficaria preso no loop eternamente. Obviamente isso provou-se falso, uma vez que descobri que a chamada asyncio.create_task(...) difere de somente atribuir a corotina a uma variável e depois "chamá-la" com await pelo fato de que o asyncio.create_task(...) já agenda ela para execução juntamente com o event loop somente aguardando uma oportunidade para executá-la (assim que o event loop volutariamente ceder o controle através de uma chamada await como no caso do await asyncio.sleep(1) dentro do loop while).
Pelas minhas pesquisas, o asyncio.gather(...) é responsável por esperar tarefas terminarem, coletar resultados, propagar exceções atuando como se fosse uma espécie de ""formalização do encerramento das tasks".

Assim, acredito eu, que tenha desvendado o "mistério" da execução correta do gabarito, rsrsrsrs.

Dúvida

Depois desse resumo, o meu entendimento está correto? Falta alguma informação importante pra fechar o quadro do motivo pelo qual no gabarito, asyncio.gather(...) seja executado após o loop?

async def exerc_07() -> None:
    """
    DESCRIPTION GOES HERE
    """
    async def processing_data(base_delay: int, index: int) -> None:
        delay: int = base_delay + ((index + 1) * random.uniform(1.0, 2.0))
        await asyncio.sleep(delay)
        results[index] = True
        print(f'{colored(f"[ALURA_ASYNC][07][{datetime.now().strftime('%H:%M:%S')}][{delay}]", "white", attrs=CGATTRS)} finished task: {colored(index, "red", attrs=CGATTRS)}')

    print(f'{colored("[ALURA_ASYNC][07]", "white", attrs=CGATTRS)} --- EXERCISE ---')
    results: List[bool] = [False, False, False]
    task_01: asyncio.Task = asyncio.create_task(processing_data(base_delay=3, index=0))
    task_02: asyncio.Task = asyncio.create_task(processing_data(base_delay=5, index=1))
    task_03: asyncio.Task = asyncio.create_task(processing_data(base_delay=7, index=2))

    while True:
        if all(results):
            print(f'{colored("[ALURA_ASYNC][07]", "white", attrs=CGATTRS)} ALL TASKS FINISHED!! Status: {colored(results, "red", attrs=CGATTRS)}')
            break

        print(f'{colored("[ALURA_ASYNC][07]", "white", attrs=CGATTRS)} status: {colored(results, "red", attrs=CGATTRS)}')
        await asyncio.sleep(1)

    await asyncio.gather(task_01, task_02, task_03)
    print(f'{colored("[ALURA_ASYNC][07]", "white", attrs=CGATTRS)} --- FINISH ---')
1 resposta

Olá Jefferson.
Tudo bem?
Seu entendimento está bem próximo do correto, só vale ajustar algumas peças importantes para fechar o quadro com precisão.
O asyncio.create_task() realmente é o ponto-chave da sua intuição: ele não “executa imediatamente”, mas registra a coroutine no event loop para execução concorrente assim que houver oportunidade. Isso significa que, a partir desse momento, o loop já pode ir alternando entre suas tasks enquanto o seu código continua rodando.
Sobre o asyncio.gather(), a sua descrição está boa, mas com um ajuste conceitual importante: ele não serve só como “formalização de encerramento”. Ele é, principalmente, uma operação de sincronização. Ele aguarda todas as tasks passadas terminarem e só então permite que o fluxo continue. Ele também coleta resultados (quando existem) e propaga exceções. Mas o ponto central aqui é: ele é um ponto de bloqueio lógico, que garante que tudo terminou antes de seguir.
Agora o ponto que geralmente causa a confusão que você descreveu: por que ele aparece depois do loop.
O seu loop já está fazendo o papel de “monitoramento ativo” das tasks. Ele não precisa do gather para executar nada; as tasks já estão rodando desde o create_task. O loop apenas observa o estado até que todas terminem.
Se você colocasse asyncio.gather() antes do loop, você estaria mudando completamente o comportamento do programa: ele deixaria de monitorar e passaria a simplesmente esperar todas as tasks terminarem de forma bloqueante (dentro da coroutine). Ou seja, o loop perderia o sentido, porque você nunca entraria nele enquanto as tasks ainda estão em execução.
Já quando o gather() vem depois, ele funciona como uma “garantia final”: mesmo que o loop tenha detectado que todas parecem concluídas, você ainda sincroniza formalmente o término das tasks. Isso é útil porque o estado booleano que você usa no results é uma forma manual de controle, mas o gather é a confirmação oficial do event loop de que não existe mais nenhuma task pendente.
Um detalhe importante: no seu caso específico, esse gather até parece redundante, porque suas tasks já terminaram quando o loop sai. Mas ele é colocado como boa prática justamente para evitar cenários em que o loop de monitoramento pode ter condições de corrida, ou em que tarefas ainda estejam finalizando pequenas partes internas.
Então resumindo a ideia central: create_task inicia concorrência, o loop apenas observa, e gather é o ponto de sincronização final que garante encerramento consistente do conjunto de tasks.
Meus parabéns pela sua abordagem...
Avise qualquer duvida.
Bons estudos.