Concorrência e paralelismo
Threads, laço de eventos, async e goroutines, e como cada modelo lida com muitos pedidos ao mesmo tempo.
Duas palavras, duas ideias
| Concorrência | Paralelismo | |
|---|---|---|
| O que é | Lidar com várias tarefas, alternando entre elas | Executar várias tarefas no mesmo instante |
| Precisa de várias CPUs | Não | Sim |
| Exemplo do cotidiano | Um garçom atende dez mesas, indo de uma a outra enquanto cada uma decide o pedido | Dez garçons atendem dez mesas ao mesmo tempo |
| Resolve | Esperas: banco, rede, disco | Cálculos: CPU ocupada |
Um backend típico passa a maior parte do tempo esperando. Uma rota consulta o banco e espera 5 milissegundos; chama outro serviço e espera 50. Enquanto um pedido espera, a CPU está livre para atender outro. Para isso, basta concorrência, e um único núcleo atende milhares de pedidos.
Paralelismo só ajuda quando o trabalho ocupa a CPU: comprimir imagens, gerar PDFs, calcular hashes de senha, processar dados. Aí a única forma de fazer mais é usar mais núcleos ao mesmo tempo.
Os modelos de concorrência
Cada linguagem resolve "atender muitos pedidos" de um jeito:
| Modelo | Como funciona | Usado por |
|---|---|---|
| Uma thread por pedido | Cada pedido ganha uma thread do sistema operacional, que pode ficar parada esperando | Servidores Java e C# tradicionais |
| Laço de eventos | Uma única thread alterna entre os pedidos sempre que um deles começa a esperar | Node, asyncio no Python |
| async e await sobre um pool | O código marca os pontos de espera; um pool pequeno de threads executa as continuações | C#, Python assíncrono, Rust |
| Tarefas leves do runtime | Milhares ou milhões de tarefas baratas, distribuídas pelo runtime entre as CPUs | Goroutines no Go, threads virtuais no Java 21 |
Os modelos diferem em quanto custa cada pedido esperando e em o que acontece quando o código faz algo que o modelo não espera. É aí que estão os problemas.
Armadilha 1: CPU pesada num laço de eventos
No Node, todo o código JavaScript roda numa única thread. Enquanto ela calcula, nenhum outro pedido é atendido.
Medimos com um servidor Node que, numa rota, calcula um hash de senha pesado. Com quatro desses pedidos em andamento, um pedido para uma rota que não faz nada levou 974 milissegundos: ele ficou na fila esperando os cálculos terminarem.
| Como o cálculo foi feito | Tempo do pedido rápido |
|---|---|
Função síncrona (pbkdf2Sync), na thread principal | 974 ms |
Função assíncrona (pbkdf2), que usa o pool de threads interno do Node | 1 ms |
A mesma operação, chamada pela versão assíncrona, foi para outra thread e deixou o laço livre. Para cálculos que não têm versão assíncrona, o Node oferece worker_threads, que rodam JavaScript em outras threads.
Armadilha 2: bloquear dentro de código assíncrono
O mesmo problema aparece no Python com asyncio, de um jeito mais sutil. Uma função async def roda no laço de eventos; se ela chamar algo que bloqueia, o laço inteiro para.
Medimos com o FastAPI, disparando 10 pedidos simultâneos a rotas que esperam 1 segundo:
| Rota | 10 pedidos simultâneos terminaram em |
|---|---|
async def com time.sleep(1) | 10,1 s |
def com time.sleep(1) | 1,0 s |
async def com await asyncio.sleep(1) | 1,0 s |
A primeira linha é o erro: time.sleep bloqueia, e como a função é async, ela bloqueia o laço. Os pedidos foram atendidos um de cada vez. A segunda funciona porque o FastAPI executa funções def comuns num pool de threads. A terceira funciona porque asyncio.sleep devolve o controle ao laço enquanto espera.
O time.sleep é só um exemplo. O mesmo acontece com drivers de banco síncronos, a biblioteca requests, leitura de arquivos e qualquer biblioteca que não seja assíncrona, chamados dentro de uma função async.
Armadilha 3: threads que não executam em paralelo
No Python padrão, uma trava global, o GIL, permite que só uma thread execute código Python por vez. Threads ajudam com esperas, mas não com cálculos.
Medimos uma tarefa de CPU que leva 0,5 segundo, executada quatro vezes numa máquina com quatro CPUs disponíveis:
| Forma | Tempo total |
|---|---|
| Uma tarefa | 0,50 s |
| Quatro tarefas em quatro threads | 2,02 s |
| Quatro tarefas em quatro processos | 0,65 s |
Com threads, o tempo foi o de rodar as quatro em sequência. Com processos, cada um com o seu próprio interpretador e o seu próprio GIL, as quatro rodaram em paralelo. O Python 3.13 tem uma versão experimental sem GIL, mas a distribuição padrão continua com ele.
Armadilha 4: dois escritores, um dado
Quando várias tarefas alteram o mesmo dado ao mesmo tempo, o resultado depende da ordem em que elas se intercalam. Isso se chama condição de corrida.
Uma operação tão simples quanto contador++ são três passos: ler o valor, somar um, gravar. Se duas tarefas leem o mesmo valor antes de qualquer uma gravar, uma das somas se perde.
Medimos em Go, com 1.000 goroutines somando 100 vezes cada uma ao mesmo contador:
| Contador | Resultado esperado | Resultado obtido |
|---|---|---|
| Variável comum, sem proteção | 100.000 | entre 72.729 e 77.182, em três execuções |
Operação atômica (atomic.Int64) | 100.000 | 100.000, sempre |
Cerca de um quarto das somas sumiu, sem nenhum erro, e com um número diferente a cada execução. O detector de corridas do Go (go run -race) apontou o problema na hora. Esse é o mesmo problema do tópico de transações, a atualização perdida, só que na memória do processo em vez do banco.
Por que isso importa quando a IA escreve o código
Assistentes escrevem código concorrente que funciona nos testes. Os problemas aparecem sob carga:
- Um cálculo pesado ou uma leitura síncrona dentro de uma rota Node.
- Uma biblioteca síncrona chamada dentro de uma função
asyncdo Python. - Um mapa ou uma lista compartilhada entre threads sem proteção.
Promise.allougathersobre mil itens, disparando mil chamadas ao mesmo tempo contra um serviço que aguenta dez.- Threads ou goroutines criadas sem limite, uma por item, até a memória acabar.
O próximo nível compara as formas de organizar o trabalho concorrente e paralelo em cada linguagem.
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.
Concorrência em cinco minutos
Explique em no máximo cinco minutos de leitura a diferença entre concorrência e paralelismo, para alguém que programa mas nunca pensou em como o servidor atende vários pedidos ao mesmo tempo. Cubra só o essencial: a diferença entre esperar e calcular, o que é um laço de eventos, por que chamar algo que bloqueia dentro de código assíncrono trava tudo, e o que é uma condição de corrida. Use a analogia de um garçom atendendo várias mesas. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Os modelos de concorrência lado a lado
Quero entender os modelos de concorrência usados no backend. Aja como um mentor. Pergunte primeiro em qual linguagem eu trabalho. Explique, um de cada vez: 1. Uma thread por pedido. 2. Laço de eventos. 3. Async e await sobre um pool de threads. 4. Tarefas leves do runtime (goroutines, threads virtuais). Para cada modelo, mostre o que acontece com mil pedidos simultâneos que esperam o banco, e com quatro pedidos que fazem um cálculo pesado. Pergunte o que eu acho que acontece antes de explicar. No final, peça que eu explique em qual modelo a minha linguagem se encaixa e qual é a armadilha principal dele.
Por que o meu servidor trava sob carga
Quero entender por que um servidor que funciona bem nos testes trava sob carga, por causa de concorrência. Explique, com um exemplo de código e uma linha do tempo para cada caso: 1. Um cálculo pesado numa rota Node. 2. Uma chamada síncrona dentro de uma rota async do Python. 3. Threads Python fazendo cálculo, e o efeito do GIL. 4. .Result em código assíncrono no C#. 5. Um bloco synchronized longo em Java. Para cada um, diga qual sintoma aparece no monitoramento (latência, CPU, filas) e como confirmar a causa. Depois me faça três perguntas de cenário e corrija as minhas respostas.
Condições de corrida
Quero entender condições de corrida de verdade. Explique: 1. Por que uma operação como contador++ não é atômica, mostrando os três passos e uma intercalação que perde uma soma. 2. Por que o defeito aparece só sob carga e quase nunca é reproduzível. 3. As formas de evitar: não compartilhar, operações atômicas, travas, estruturas concorrentes, mensagens e o banco de dados. 4. A relação com a atualização perdida em transações de banco. Depois me mostre quatro trechos de código, um por vez, e pergunte se cada um tem condição de corrida. Inclua um mapa compartilhado, um contador, uma inicialização preguiçosa e um caso correto. Corrija as minhas respostas.