DevGuideTK
← Trilha Backend

Concorrência e paralelismo

Threads, laço de eventos, async e goroutines, e como cada modelo lida com muitos pedidos ao mesmo tempo.

As decisões de concorrência são quatro: como tratar trabalho de CPU, como chamar código que bloqueia, como proteger dados compartilhados e como limitar quanto trabalho acontece ao mesmo tempo.

Trabalho de entrada e saída e trabalho de CPU

Tipo de trabalhoExemploO que ajudaO que não ajuda
Entrada e saídaConsultas ao banco, chamadas HTTP, leitura de arquivosConcorrência: async, laço de eventos, goroutines, threads virtuaisMais CPUs, se o gargalo é a espera
CPUHash de senha, compressão, PDF, processamento de imagens, cálculosParalelismo: várias CPUs, processos, trabalhadores separadosAsync: calcular não tem ponto de espera para devolver o controle

Trabalho de CPU em cada linguagem

LinguagemOnde colocar o trabalho de CPU
NodeVersões assíncronas das funções nativas (crypto.pbkdf2, zlib.gzip); worker_threads para JavaScript pesado; ou uma fila com trabalhadores separados
PythonVários processos (ProcessPoolExecutor, vários trabalhadores do Uvicorn ou Gunicorn); bibliotecas nativas que liberam o GIL, como NumPy; ou uma fila
GoGoroutines já executam em paralelo, até o GOMAXPROCS
JavaUm pool de threads de plataforma dimensionado pelas CPUs; threads virtuais não aceleram cálculo
C#Task.Run ou Parallel.For para levar o cálculo ao pool de threads

Para cálculos demorados, que levam segundos, a melhor saída costuma ser tirar o trabalho do pedido: publicar numa fila, processar em trabalhadores separados e avisar quando terminar. O tópico de filas e o de tarefas em segundo plano mostram como.

Código que bloqueia em contexto assíncrono

LinguagemComo chamar algo que bloqueia sem travar o laço ou o pool
NodeUsar a versão assíncrona da função; nunca as versões Sync no caminho de um pedido
Pythonawait asyncio.to_thread(funcao, ...), ou declarar a rota com def em vez de async def no FastAPI, ou trocar a biblioteca por uma assíncrona
GoNão há laço a travar: goroutines podem bloquear à vontade
JavaCom threads virtuais, bloquear é barato; evite só blocos synchronized longos, que em versões anteriores ao Java 24 prendem a thread de plataforma
C#Usar as versões Async das APIs; nunca .Result ou .Wait() em código assíncrono, que esgotam o pool de threads

No Python, a escolha entre def e async def no FastAPI tem uma regra simples: use async def só se tudo o que a função chama for assíncrono. Na dúvida, def é mais seguro, porque roda num pool de threads.

Proteger dados compartilhados

A forma mais segura de evitar condições de corrida é não compartilhar estado mutável. Quando é preciso compartilhar:

TécnicaComo funcionaQuando usar
Não compartilharCada tarefa trabalha com os seus dados e devolve um resultadoSempre que possível
Operações atômicasSomar, trocar ou comparar um valor numa única instrução indivisívelContadores e indicadores simples
Trava (mutex, lock)Só uma tarefa por vez acessa o trecho protegidoEstruturas que precisam de várias operações juntas
Estruturas concorrentesMapas e filas feitos para acesso simultâneo (ConcurrentHashMap, ConcurrentDictionary, sync.Map)Caches e registros compartilhados
Canais e mensagensAs tarefas trocam dados por mensagens, em vez de dividir memóriaFluxos de trabalho em Go, filas internas
O banco de dadosTransações e gravações condicionaisEstado que precisa sobreviver e ser visto por várias instâncias

Lembre que estado na memória do processo só é compartilhado dentro de uma instância. Com várias cópias da aplicação, o contador em memória de cada uma é diferente, como mostra o tópico de escalabilidade. Estado que importa entre pedidos costuma pertencer ao banco ou ao Redis, e não a uma variável.

Limitar a concorrência

Concorrência sem limite é o erro mais comum ao acelerar código com async. Um laço que dispara uma chamada por item, todas ao mesmo tempo, transforma uma lista de mil itens em mil chamadas simultâneas.

O que acontece sem limiteConsequência
Mil conexões simultâneas a outro serviçoO serviço recusa, limita ou cai
Mil consultas simultâneas ao bancoO pool de conexões esgota, e os outros pedidos esperam
Mil threads ou goroutines com dados em memóriaO processo fica sem memória
Mil chamadas a uma API externaA API devolve erros de limite de requisições
LinguagemFerramenta para limitar
NodeUm grupo fixo de trabalhadores que consomem uma lista, ou bibliotecas como p-limit
Pythonasyncio.Semaphore
Goerrgroup com SetLimit, ou um semáforo com canal
JavaSemaphore, mesmo com threads virtuais
C#Parallel.ForEachAsync com MaxDegreeOfParallelism, ou SemaphoreSlim

O limite certo vem da dependência mais fraca: o que o serviço chamado aguenta, o tamanho do pool de conexões, o limite de requisições da API.

Resumo da decisão

  1. Identifique o tipo de trabalho: espera pede concorrência, cálculo pede paralelismo.
  2. CPU pesada fora do laço de eventos: pool de threads, processos ou fila.
  3. Nada que bloqueia dentro de código assíncrono.
  4. Estado compartilhado: evite; se precisar, atômicos, travas ou estruturas concorrentes; entre instâncias, o banco.
  5. Toda concorrência tem limite, definido pela dependência mais fraca.

O próximo nível mostra chamadas concorrentes com limite nas cinco linguagens e as ferramentas para detectar os problemas deste tópico.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Onde colocar o trabalho de CPU

    Minha aplicação tem trabalho pesado de CPU (por exemplo, gerar PDFs, processar imagens ou calcular hashes) e quero decidir onde ele deve rodar.
    
    Pergunte qual linguagem e framework eu uso, quanto tempo cada tarefa leva e quantas acontecem por minuto.
    
    Compare, para o meu caso:
    1. Fazer dentro do pedido, na thread atual.
    2. Mandar para um pool de threads ou para worker threads.
    3. Usar vários processos.
    4. Publicar numa fila e processar em trabalhadores separados.
    
    Para cada opção, diga o efeito no tempo de resposta, na capacidade do servidor e na complexidade. Termine com a recomendação e com a forma de avisar o usuário quando a tarefa terminar, se ela sair do pedido.
  • Async sem armadilhas na minha linguagem

    Quero usar código assíncrono corretamente na minha linguagem, sem travar o laço de eventos ou o pool de threads.
    
    Pergunte qual linguagem, framework e bibliotecas de banco e HTTP eu uso.
    
    Explique, para o meu caso:
    1. Quais bibliotecas que eu uso são síncronas e quais são assíncronas.
    2. Como chamar uma biblioteca síncrona a partir de código assíncrono sem travar nada.
    3. As regras do meu framework para escolher entre funções síncronas e assíncronas.
    4. Como medir o atraso do laço de eventos ou a fila do pool de threads em produção.
    5. Os erros mais comuns da minha linguagem com async.
    
    Termine com uma lista curta de regras para a equipe.
  • Limitar a concorrência

    Preciso fazer muitas chamadas a outro serviço ou ao banco ao mesmo tempo, e quero fazer isso sem derrubar a dependência.
    
    Pergunte qual linguagem eu uso, quantos itens há e quais são os limites da dependência (conexões, requisições por segundo, tamanho do pool).
    
    Explique:
    1. O que acontece quando a concorrência não tem limite.
    2. Como escolher o limite a partir da dependência mais fraca.
    3. A ferramenta de limite da minha linguagem, com código.
    4. Como tratar falhas: cancelar o resto ou continuar.
    5. Como combinar o limite com tempo limite e novas tentativas.
    
    Termine com o código final para o meu caso.