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.

O caso mais comum de concorrência no dia a dia do backend: buscar vários itens em outro serviço, ao mesmo tempo, sem derrubar esse serviço. Os exemplos buscam 20 pedidos com no máximo 5 chamadas simultâneas.

Chamadas concorrentes com limite

// Executa a tarefa para cada item, com no máximo `limite` ao mesmo tempo
async function comLimite<T, R>(
  itens: T[],
  limite: number,
  tarefa: (item: T) => Promise<R>,
): Promise<R[]> {
  const resultados = new Array<R>(itens.length);
  let proximo = 0;

  async function trabalhador() {
    while (proximo < itens.length) {
      const indice = proximo++;
      resultados[indice] = await tarefa(itens[indice]);
    }
  }

  const quantidade = Math.min(limite, itens.length);
  await Promise.all(Array.from({ length: quantidade }, trabalhador));
  return resultados;
}

const ids = Array.from({ length: 20 }, (_, i) => i + 1);

const pedidos = await comLimite(ids, 5, async (id) => {
  const resposta = await fetch(`${BASE_URL}/pedidos/${id}`, {
    signal: AbortSignal.timeout(3000),
  });
  if (!resposta.ok) throw new Error(`pedido ${id}: HTTP ${resposta.status}`);
  return resposta.json();
});

O que muda de uma linguagem para outra

Ferramenta do limiteSe uma chamada falhar
NodeGrupo fixo de trabalhadoresPromise.all rejeita; as chamadas em andamento continuam até terminar
Pythonasyncio.Semaphoregather propaga a primeira exceção; as outras tarefas continuam, salvo com TaskGroup
Goerrgroup com SetLimitWithContext cancela o contexto, e as chamadas que respeitam o contexto param
JavaSemaphore com threads virtuaisfuturo.get() lança a exceção da tarefa; as outras continuam
C#Parallel.ForEachAsyncPara de iniciar novas e cancela o token das que estão em andamento

A coluna da direita importa quando uma falha deve interromper o resto. Em Python, asyncio.TaskGroup cancela as outras tarefas quando uma falha. Em Node, passe um AbortController compartilhado para as chamadas e aborte-o no primeiro erro.

Repare também como os resultados são guardados: em Go, cada goroutine grava só na sua posição do slice, o que é seguro; em C#, um ConcurrentDictionary. Um List comum em C#, ou um mapa comum em Go, alterados por várias tarefas ao mesmo tempo, seriam uma condição de corrida.

Detectar os problemas

ProblemaFerramenta
Laço de eventos travado no Nodeperf_hooks.monitorEventLoopDelay(), ou a opção --cpu-prof para ver o que ocupa a thread
Chamada bloqueante em asyncioModo de depuração: PYTHONASYNCIODEBUG=1 avisa quando um passo do laço demora mais que 100 ms
Condição de corrida em Gogo test -race e go run -race
Condição de corrida em Java e C#Revisão de código e ferramentas de análise estática; testes de estresse com muitas threads
Esgotamento do pool de threads no .NETO contador threadpool-queue-length no dotnet-counters
Goroutines vazandoruntime.NumGoroutine() exposto como métrica, e o perfil de goroutines do pprof

O detector de corridas do Go merece destaque: no teste deste tópico, ele apontou a corrida no contador na primeira execução, enquanto o programa sem o detector só mostrava um número errado. Rodar os testes com -race na integração contínua pega esse tipo de defeito antes da produção.

Medir o atraso do laço de eventos no Node é simples:

import { monitorEventLoopDelay } from "node:perf_hooks";

const atraso = monitorEventLoopDelay({ resolution: 20 });
atraso.enable();

setInterval(() => {
  // p99 do atraso do laço, em milissegundos, nos últimos 10 segundos
  console.log({ atrasoLacoP99Ms: atraso.percentile(99) / 1e6 });
  atraso.reset();
}, 10_000);

Um p99 acima de algumas dezenas de milissegundos indica que algo está ocupando a thread principal.

Lista de revisão

Laço de eventos e async

  • Nenhuma função síncrona pesada (*Sync no Node, bibliotecas síncronas no Python) roda no caminho de um pedido?
  • Funções async def no Python chamam só código assíncrono?
  • Trabalho de CPU pesado vai para outra thread, processo ou fila?
  • No C#, não há .Result, .Wait() nem async void?

Dados compartilhados

  • Variáveis, mapas e listas alterados por várias tarefas estão protegidos?
  • O estado compartilhado precisa mesmo estar na memória, ou pertence ao banco?
  • Os testes de Go rodam com -race?

Limites

  • Toda chamada concorrente em massa tem um limite explícito?
  • O limite respeita o que a dependência mais fraca aguenta?
  • Há tempo limite em cada chamada?
  • Uma falha cancela as outras quando isso é o desejado?

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.

  • Reproduzir as armadilhas de concorrência

    Quero reproduzir na prática as principais armadilhas de concorrência da minha linguagem, para reconhecê-las depois.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas, conforme a linguagem:
    1. Um servidor com uma rota que faz um cálculo pesado e outra rápida; medir a rota rápida enquanto a pesada roda.
    2. Corrigir levando o cálculo para fora da thread principal, e medir de novo.
    3. Uma chamada bloqueante dentro de código assíncrono, com dez pedidos simultâneos, antes e depois da correção.
    4. Um contador compartilhado entre muitas tarefas, sem proteção e com proteção, e o detector de corridas se a linguagem tiver.
    5. Mil chamadas concorrentes sem limite contra um servidor de teste que aceita só dez por vez, antes e depois de acrescentar um limite.
    
    Em cada etapa, peça que eu preveja o resultado antes de rodar.
  • Revisar código concorrente

    Vou colar abaixo código que faz trabalho concorrente ou paralelo. Revise com foco em correção e em comportamento sob carga.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando a linha:
    1. Há trabalho pesado de CPU no laço de eventos ou numa thread que atende pedidos?
    2. Há chamadas bloqueantes dentro de código assíncrono?
    3. Há dados compartilhados alterados por várias tarefas sem proteção?
    4. Há chamadas concorrentes sem limite?
    5. Há tempo limite em cada chamada?
    6. As falhas cancelam o resto quando deveriam?
    7. Há tarefas que podem nunca terminar, como goroutines esperando um canal que ninguém fecha?
    
    Para cada problema, descreva o que acontece sob carga e mostre a correção. Não reescreva o que já está correto.
    
    Código:
    [cole aqui]
  • Monitorar a concorrência em produção

    Quero monitorar em produção os sinais de problemas de concorrência na minha aplicação.
    
    Pergunte qual linguagem e plataforma de monitoramento eu uso.
    
    Explique, com código para o meu caso:
    1. Como medir o atraso do laço de eventos (Node, Python) ou a fila do pool de threads (.NET, Java).
    2. Como acompanhar o número de goroutines ou threads ativas.
    3. Como medir quantas chamadas a cada dependência estão em andamento ao mesmo tempo.
    4. Quais valores devem gerar alerta, e por quê.
    5. Como investigar quando o alerta disparar: perfis de CPU, despejos de threads, perfil de goroutines.