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();
});import asyncio
import httpx
async def buscar_pedidos(ids: list[int], limite: int = 5) -> list[dict]:
semaforo = asyncio.Semaphore(limite)
async with httpx.AsyncClient(base_url=BASE_URL, timeout=3) as cliente:
async def buscar(pedido_id: int) -> dict:
async with semaforo: # no máximo `limite` ao mesmo tempo
resposta = await cliente.get(f"/pedidos/{pedido_id}")
resposta.raise_for_status()
return resposta.json()
return await asyncio.gather(*(buscar(i) for i in ids))
pedidos = asyncio.run(buscar_pedidos(list(range(1, 21))))package pedidos
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
var cliente = &http.Client{Timeout: 3 * time.Second}
func BuscarPedidos(ctx context.Context, baseURL string, ids []int) ([]map[string]any, error) {
resultados := make([]map[string]any, len(ids))
grupo, ctx := errgroup.WithContext(ctx)
grupo.SetLimit(5) // no máximo 5 goroutines ao mesmo tempo
for i, id := range ids {
grupo.Go(func() error {
pedido, err := buscarPedido(ctx, baseURL, id)
if err != nil {
return err // cancela o contexto das outras
}
resultados[i] = pedido // cada goroutine grava só a sua posição
return nil
})
}
return resultados, grupo.Wait()
}
func buscarPedido(ctx context.Context, baseURL string, id int) (map[string]any, error) {
pedido, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/pedidos/%d", baseURL, id), nil)
resposta, err := cliente.Do(pedido)
if err != nil {
return nil, err
}
defer resposta.Body.Close()
if resposta.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pedido %d: HTTP %d", id, resposta.StatusCode)
}
var corpo map[string]any
return corpo, json.NewDecoder(resposta.Body).Decode(&corpo)
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
public class BuscaDePedidos {
private static final HttpClient CLIENTE = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
public static List<String> buscar(String baseUrl, List<Integer> ids) throws Exception {
var limite = new Semaphore(5); // no máximo 5 ao mesmo tempo
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futuros = new ArrayList<>();
for (int id : ids) {
futuros.add(executor.submit(() -> {
limite.acquire();
try {
return buscarPedido(baseUrl, id);
} finally {
limite.release();
}
}));
}
List<String> pedidos = new ArrayList<>();
for (var futuro : futuros) {
pedidos.add(futuro.get());
}
return pedidos;
}
}
static String buscarPedido(String baseUrl, int id) throws Exception {
var pedido = HttpRequest.newBuilder(URI.create(baseUrl + "/pedidos/" + id))
.timeout(Duration.ofSeconds(3))
.build();
var resposta = CLIENTE.send(pedido, HttpResponse.BodyHandlers.ofString());
if (resposta.statusCode() != 200) {
throw new IllegalStateException("pedido " + id + ": HTTP " + resposta.statusCode());
}
return resposta.body();
}
}using System.Collections.Concurrent;
using System.Net.Http.Json;
public class BuscaDePedidos(HttpClient cliente)
{
public async Task<IReadOnlyDictionary<int, Pedido>> Buscar(IEnumerable<int> ids)
{
var pedidos = new ConcurrentDictionary<int, Pedido>();
var opcoes = new ParallelOptions { MaxDegreeOfParallelism = 5 };
await Parallel.ForEachAsync(ids, opcoes, async (id, cancelamento) =>
{
var pedido = await cliente.GetFromJsonAsync<Pedido>($"/pedidos/{id}", cancelamento)
?? throw new InvalidOperationException($"pedido {id} vazio");
pedidos[id] = pedido;
});
return pedidos;
}
}
public record Pedido(int Id, string Status);O que muda de uma linguagem para outra
| Ferramenta do limite | Se uma chamada falhar | |
|---|---|---|
| Node | Grupo fixo de trabalhadores | Promise.all rejeita; as chamadas em andamento continuam até terminar |
| Python | asyncio.Semaphore | gather propaga a primeira exceção; as outras tarefas continuam, salvo com TaskGroup |
| Go | errgroup com SetLimit | WithContext cancela o contexto, e as chamadas que respeitam o contexto param |
| Java | Semaphore com threads virtuais | futuro.get() lança a exceção da tarefa; as outras continuam |
| C# | Parallel.ForEachAsync | Para 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
| Problema | Ferramenta |
|---|---|
| Laço de eventos travado no Node | perf_hooks.monitorEventLoopDelay(), ou a opção --cpu-prof para ver o que ocupa a thread |
Chamada bloqueante em asyncio | Modo de depuração: PYTHONASYNCIODEBUG=1 avisa quando um passo do laço demora mais que 100 ms |
| Condição de corrida em Go | go 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 .NET | O contador threadpool-queue-length no dotnet-counters |
| Goroutines vazando | runtime.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 (
*Syncno Node, bibliotecas síncronas no Python) roda no caminho de um pedido? - Funções
async defno 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()nemasync 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.