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 trabalho | Exemplo | O que ajuda | O que não ajuda |
|---|---|---|---|
| Entrada e saída | Consultas ao banco, chamadas HTTP, leitura de arquivos | Concorrência: async, laço de eventos, goroutines, threads virtuais | Mais CPUs, se o gargalo é a espera |
| CPU | Hash de senha, compressão, PDF, processamento de imagens, cálculos | Paralelismo: várias CPUs, processos, trabalhadores separados | Async: calcular não tem ponto de espera para devolver o controle |
Trabalho de CPU em cada linguagem
| Linguagem | Onde colocar o trabalho de CPU |
|---|---|
| Node | Versões assíncronas das funções nativas (crypto.pbkdf2, zlib.gzip); worker_threads para JavaScript pesado; ou uma fila com trabalhadores separados |
| Python | Vários processos (ProcessPoolExecutor, vários trabalhadores do Uvicorn ou Gunicorn); bibliotecas nativas que liberam o GIL, como NumPy; ou uma fila |
| Go | Goroutines já executam em paralelo, até o GOMAXPROCS |
| Java | Um 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
| Linguagem | Como chamar algo que bloqueia sem travar o laço ou o pool |
|---|---|
| Node | Usar a versão assíncrona da função; nunca as versões Sync no caminho de um pedido |
| Python | await 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 |
| Go | Não há laço a travar: goroutines podem bloquear à vontade |
| Java | Com 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écnica | Como funciona | Quando usar |
|---|---|---|
| Não compartilhar | Cada tarefa trabalha com os seus dados e devolve um resultado | Sempre que possível |
| Operações atômicas | Somar, trocar ou comparar um valor numa única instrução indivisível | Contadores e indicadores simples |
Trava (mutex, lock) | Só uma tarefa por vez acessa o trecho protegido | Estruturas que precisam de várias operações juntas |
| Estruturas concorrentes | Mapas e filas feitos para acesso simultâneo (ConcurrentHashMap, ConcurrentDictionary, sync.Map) | Caches e registros compartilhados |
| Canais e mensagens | As tarefas trocam dados por mensagens, em vez de dividir memória | Fluxos de trabalho em Go, filas internas |
| O banco de dados | Transações e gravações condicionais | Estado 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 limite | Consequência |
|---|---|
| Mil conexões simultâneas a outro serviço | O serviço recusa, limita ou cai |
| Mil consultas simultâneas ao banco | O pool de conexões esgota, e os outros pedidos esperam |
| Mil threads ou goroutines com dados em memória | O processo fica sem memória |
| Mil chamadas a uma API externa | A API devolve erros de limite de requisições |
| Linguagem | Ferramenta para limitar |
|---|---|
| Node | Um grupo fixo de trabalhadores que consomem uma lista, ou bibliotecas como p-limit |
| Python | asyncio.Semaphore |
| Go | errgroup com SetLimit, ou um semáforo com canal |
| Java | Semaphore, 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
- Identifique o tipo de trabalho: espera pede concorrência, cálculo pede paralelismo.
- CPU pesada fora do laço de eventos: pool de threads, processos ou fila.
- Nada que bloqueia dentro de código assíncrono.
- Estado compartilhado: evite; se precisar, atômicos, travas ou estruturas concorrentes; entre instâncias, o banco.
- 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.