23 pontos por darjeeling 2025-11-16 | 12 comentários | Compartilhar no WhatsApp

Causas da queda de desempenho em código assíncrono e como resolver (resumo técnico)

Este vídeo aborda as causas mais comuns de o código asyncio em Python ficar mais lento do que o código síncrono e as abordagens técnicas para corrigir isso.

1. Conceitos centrais do Asyncio

  • Loop de eventos (Event Loop): é o núcleo de toda aplicação assíncrona. Ele é iniciado com asyncio.run() e gerencia e agenda a execução de tarefas em uma única thread.
  • Corrotinas (Coroutines): são funções assíncronas declaradas com async def. Ao encontrar a palavra-chave await, podem pausar a execução e devolver o controle ao loop de eventos.
  • Tarefas (Tasks): encapsulam corrotinas e as agendam para execução concorrente no loop de eventos. São criadas com asyncio.create_task().
  • Futures: são objetos de baixo nível que representam o resultado final de uma operação assíncrona.

2. Exemplo de conversão de código síncrono para assíncrono

Substitui-se o time.sleep() síncrono por await asyncio.sleep() assíncrono, declara-se a função com async def e executa-se a corrotina principal com asyncio.run().


Erros comuns que causam queda de desempenho e suas soluções

Erro 1: execução sequencial (Sequential Execution)

Se tarefas independentes forem aguardadas com await de forma sequencial em vez de executadas em paralelo, o tempo total de execução será a soma do tempo de todas as tarefas.

  • Exemplo incorreto (sequencial):

    # Cada await espera o término da operação anterior  
    await get_user_notifications()  
    await get_recent_activity()  
    await get_unread_messages()  
    
  • Solução (paralelo): use asyncio.gather ou asyncio.TaskGroup para executar tarefas independentes ao mesmo tempo. O tempo total cai para o da tarefa mais demorada.

    # As três operações começam ao mesmo tempo  
    await asyncio.gather(  
        get_user_notifications(),  
        get_recent_activity(),  
        get_unread_messages()  
    )  
    

Comparação entre ferramentas de execução paralela

  • asyncio.gather:
    • Executa várias corrotinas ao mesmo tempo.
    • Desvantagem: o tratamento de erros é limitado. Se uma tarefa gerar exceção, outras tarefas em execução podem ser canceladas.
  • asyncio.create_task:
    • Permite controle e tratamento de erro por tarefa.
    • É útil para execução em segundo plano, mas traz o incômodo de precisar fazer await de várias tarefas individualmente.
  • asyncio.TaskGroup (Python 3.11+):
    • É a alternativa moderna para “concorrência estruturada”.
    • Usa a sintaxe async with para gerenciar um grupo de tarefas, garantindo que ao sair do contexto todas as tarefas tenham sido concluídas ou tratado exceções.
    async with asyncio.TaskGroup() as tg:  
        tg.create_task(some_coro_1())  
        tg.create_task(some_coro_2())  
    # Quando o bloco 'async with' termina, todas as tarefas já foram aguardadas  
    

Erro 2: uso de bibliotecas síncronas

Se você usar bibliotecas síncronas (bloqueantes), como requests ou pathlib, dentro de código asyncio, todo o loop de eventos fica bloqueado. Mesmo dentro de asyncio.gather, na prática o comportamento será sequencial.

  • Solução: use bibliotecas dedicadas com suporte assíncrono (não bloqueante), como aiohttp (no lugar de requests) e aiofiles (no lugar de files/pathlib).

Erro 3: bloqueio do loop de eventos por trabalho CPU-bound

Como o asyncio roda em uma única thread, cálculos pesados (CPU-bound) param o loop de eventos e atrasam outras operações de I/O.

  • Solução: use loop.run_in_executor() para descarregar trabalho CPU-bound em um pool de threads separado (padrão) ou em um pool de processos.
    loop = asyncio.get_running_loop()  
    # Executa a função intensiva em CPU em uma thread separada  
    await loop.run_in_executor(  
        None,  # Usa o pool de threads padrão  
        cpu_bound_function,  
        arg1  
    )  
    

Erro 4: bloqueio causado por trabalho não essencial

Se você fizer await em tarefas não essenciais, como logging sem relação com a resposta ao usuário, o tempo de resposta aumenta sem necessidade.

  • Solução: use asyncio.create_task() para separar esse trabalho como tarefa em segundo plano e não faça await.
    user_profile = await get_user_profile()  
    # Executa o logging em segundo plano sem await  
    asyncio.create_task(send_logs_to_external_service())  
    return user_profile  
    

Erro 5: criação de tarefas em excesso

Transformar uma grande quantidade de operações muito pequenas em tarefas pode gerar overhead de troca de contexto e piorar o desempenho.

  • Solução 1: agrupe operações pequenas em lotes (batching) para formar menos tarefas e mais robustas.
  • Solução 2: use asyncio.Semaphore para limitar o número máximo de tarefas executadas ao mesmo tempo.
    # Permite no máximo 10 tarefas simultâneas  
    semaphore = asyncio.Semaphore(10)  
    
    async with semaphore:  
        await fetch_data()  
    

Outros erros

  • Corrotinas “Never Awaited”: você chama a corrotina mas não faz await, então a tarefa nem chega a executar e falha silenciosamente. Isso pode ser detectado com linters como flake8-async.
  • Gerenciamento inadequado de recursos: usar arquivos, conexões de banco de dados etc. sem try...finally pode causar vazamento de recursos. A solução é usar gerenciadores de contexto assíncronos com async with.

Depuração e escolha do modelo de concorrência

Modo de depuração do Asyncio

Ao ativar o modo de depuração, que vem desativado por padrão (asyncio.run(debug=True)), fica mais fácil detectar problemas como:

  • corrotinas sem await (RuntimeWarning).
  • APIs assíncronas chamadas da thread errada.
  • callbacks com tempo de execução acima de 100ms.
  • operações lentas do seletor de I/O (selector).

Outras ferramentas de depuração

  • Scalene: profiler de CPU e memória.
  • aio-monitor: monitoramento e CLI para aplicações asyncio.
  • pdb: depurador padrão do Python.
  • py-stack: imprime stack traces de processos Python em execução para detectar pontos de bloqueio.

Guia para escolher o modelo de concorrência

  • Asyncio (thread única): ideal para grande volume de tarefas I/O-bound com alta latência, como requisições de rede e I/O de arquivos.
  • Threads (multithread): usadas para tarefas I/O-bound que precisam de acesso a dados compartilhados. Por causa do GIL (Global Interpreter Lock), não há paralelismo real, mas outras threads podem rodar enquanto uma espera por I/O.
  • Processes (multiprocessos): usados para tarefas CPU-bound, como processamento de imagem e cálculos pesados. Aproveitam vários núcleos de CPU para obter paralelismo real, mas têm alto overhead de memória e comunicação.

https://youtu.be/wGDOwNW6lVk

12 comentários

 
savvykang 2025-11-18

Python é de fato uma excelente linguagem, mas a interface assíncrona parece ser um recurso mal projetado.

 
ceruns 2025-11-17

Faltou o eager_start=True no item 4. Como create_task cria uma weakref, esse código pode acabar gerando uma tarefa que nunca será executada...

 
tested 2025-11-17

> https://rosettalens.com/s/ko/python-to-node

Parece que essa pessoa também migrou para o Node.js por causa do async do Python

 
kandk 2025-11-17

Conclusão: a interface assíncrona do Python ainda não é intuitiva.

 
bungker 2025-11-17

Na verdade, se o projeto já está em um nível em que vale a pena otimizar a programação assíncrona em Python, em termos de desempenho e estabilidade é muito melhor escrevê-lo em outra linguagem.

 
euphcat 2025-11-17

Se não for para uma linguagem compilada, a diferença de desempenho é tão grande assim? Em multithreading até faria sentido, já que a existência do GIL cria uma diferença grande, mas se no fim das contas for uma estrutura assíncrona em que o event loop está rodando, fico curioso sobre que tipo de diferença surge dependendo da linguagem.

 
vwjdalsgkv 2025-11-17

A presença ou ausência de compilação JIT faz mais diferença do que se imagina. O V8 é muito bem otimizado.

 
euphcat 2025-11-16

Não conferi o vídeo de origem, mas o código de solução para o erro 4 está incorreto.

A instância de task retornada por create_task() precisa ser atribuída a pelo menos uma variável, e essa variável deve continuar existindo até a task terminar. Caso contrário, há o risco de a instância da task ser coletada pelo garbage collector enquanto a coroutine ainda está em execução.

Se a função que cria a task terminar logo em seguida, como no caso acima, é preciso usar métodos como retornar a instância da task, atribuí-la a uma variável global ou a uma variável de instância.

P.S.)
Mesmo que você realmente não precise do valor de retorno e tenha certeza de que a coroutine vai terminar em pouco tempo, ainda assim é melhor programar de forma que em algum momento seja feito um await sobre a instância da task. Se não quiser fazer isso, então o ideal é montar uma estrutura em que cada coroutine que vai rodar como task tenha um tratamento de exceções bem rigoroso, com mensagens de log emitidas sem falhas. Caso contrário, pode acontecer de, por maior que seja o problema causado pela task, a Exception não ser tratada e falhar silenciosamente.

Num projeto que eu desenvolvo e administro profissionalmente, já cheguei a desenhar um padrão em que dezenas de módulos criavam cada um uma task no estilo while self.ok(): cmd = await self.cmd_queue.get(); await self.process(cmd); e a mantinham rodando continuamente. Até estabelecer um padrão de tratamento de exceções, cada problema que aparecia vinha acompanhado de uma experiência rara de ver minha sanidade ir junto pelos ares, haha

 
kunggom 2025-11-16

Mesmo do ponto de vista de alguém que trabalha em uma empresa que usa C#, que pode ser considerado o precursor(?) do padrão Async/Await, ainda vejo com bastante frequência código incorreto como o erro nº 1, em que await é simplesmente colocado em sequência, um atrás do outro.

Quando vejo esse tipo de código, tenho a impressão de que, em comum, a pessoa só sabe que precisa usar a palavra-chave await antes de chamar um método async, mas não pensa muito além disso sobre a ordem de execução assíncrona, e é por isso que esse tipo de código aparece.
Quando há vários await, alguns resultados são usados logo na linha de baixo, então faz sentido receber antes o resultado do await do objeto Task<T>; outros só serão usados bem mais adiante, então dá para receber apenas o Task<T> e fazer await depois. Escrever código levando em conta esse fluxo assíncrono exige justamente esse nível de raciocínio.

Pelo menos eu costumo escrever métodos declarados como assíncronos considerando esse fluxo de processamento, mas às vezes, ao ver código legado de alguém que saiu da empresa e eu preciso manter, tenho a sensação de estar lendo algo como: “Eu só queria escrever código síncrono de forma simples, mas como o método que preciso usar no meio do caminho só existe em versão assíncrona, vou escrever assim mesmo.”

 
skageektp 2025-11-17

Se o item 1 for sempre independente, fazer desse jeito até que é bom,
mas se o código for alterado e deixar de ser independente, parece que também há o incômodo de ter que revisar e corrigir todos os lugares que usam essa função.
Se for uma tarefa que não leva tanto tempo assim, talvez fazer await em série seja melhor do ponto de vista de manutenção do código.

 
euphcat 2025-11-17

Acho que faz sentido abordar isso com a ideia de que “como o overhead do multithreading é pesado, a alternativa é dividir uma única thread para resolver o processamento paralelo”. Por isso, no fim das contas, parece correto dizer que, por padrão, dependendo do caso, isso exige ainda mais cuidado do que o multithreading.

 
kunggom 2025-11-17

Isso também é verdade.
Parece que um código assíncrono bem feito é, por natureza, o tipo de código que exige muita atenção.