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.

Duas palavras, duas ideias

ConcorrênciaParalelismo
O que éLidar com várias tarefas, alternando entre elasExecutar várias tarefas no mesmo instante
Precisa de várias CPUsNãoSim
Exemplo do cotidianoUm garçom atende dez mesas, indo de uma a outra enquanto cada uma decide o pedidoDez garçons atendem dez mesas ao mesmo tempo
ResolveEsperas: banco, rede, discoCá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:

ModeloComo funcionaUsado por
Uma thread por pedidoCada pedido ganha uma thread do sistema operacional, que pode ficar parada esperandoServidores Java e C# tradicionais
Laço de eventosUma única thread alterna entre os pedidos sempre que um deles começa a esperarNode, asyncio no Python
async e await sobre um poolO código marca os pontos de espera; um pool pequeno de threads executa as continuaçõesC#, Python assíncrono, Rust
Tarefas leves do runtimeMilhares ou milhões de tarefas baratas, distribuídas pelo runtime entre as CPUsGoroutines 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 feitoTempo do pedido rápido
Função síncrona (pbkdf2Sync), na thread principal974 ms
Função assíncrona (pbkdf2), que usa o pool de threads interno do Node1 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:

Rota10 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:

FormaTempo total
Uma tarefa0,50 s
Quatro tarefas em quatro threads2,02 s
Quatro tarefas em quatro processos0,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:

ContadorResultado esperadoResultado obtido
Variável comum, sem proteção100.000entre 72.729 e 77.182, em três execuções
Operação atômica (atomic.Int64)100.000100.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 async do Python.
  • Um mapa ou uma lista compartilhada entre threads sem proteção.
  • Promise.all ou gather sobre 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.