O que acontece quando você tenta usar todos os núcleos
Eu fui parar em um projeto que processava arquivos de log de 2 GB usando quatro threads, cada uma carregando um quarto do arquivo na memória. A princípio, parecia perfeito. O throughput triplicou em relação à execução sequencial. Mas quando elevamos para oito threads, o tempo de resposta simplesmente piorou. Não piorou um pouco. Piorou de forma mensurável e consistente. O gargalo não era o disco, nem a CPU, nem a rede. Era a sincronização.
multiprocessador qual o melhor
A pergunta mais comum que vejo em fóruns e reuniões de arquitetura nunca tem uma resposta única. O hardware certo depende da carga. Se seu processamento é fortemente dependente de E/S, como leitura de banco de dados ou requisições HTTP, oito núcleos virtuais podem ser suficientes. Se a carga é computacional pura, como renderização, compressão ou criptografia, aí sim você começa a notar ganhos lineares com mais núcleos físicos, e aí sim o custo por núcleo cai consideravelmente. O melhor multiprocessador é aquele que corresponde ao padrão de acesso à memória e ao tipo de operação da sua carga.
Como estruturar o trabalho
O primeiro erro é criar uma thread por tarefa e esperar que o sistema operacional gere escalonamento automático. Em ambientes de servidor com centenas de requisições concorrentes, isso gera consumo excessivo de memória e context switching. A abordagem correta é fixar o pool de threads ao número de núcleos físicos disponíveis, ou usar work-stealing se as tarefas tiverem duração imprevisível. Para tarefas homogêneas, como processamento de imagens ou cálculos numéricos, divida os dados em chunks de tamanho fixo antes de distribuir. Um chunk de 10 MB costuma ser um bom ponto de partida para CPU bound. Para E/S bound, chunks menores reduzem a latência percebida, mas aumentam a sobrecarga de gerenciamento. Eu costumo usar 1 MB como padrão inicial e ajustar conforme o profiling.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Problema real que enfrentei
Em um pipeline de transformação de dados, tínhamos dois workers lendo de filas distintas e escrevendo em um mesmo arquivo de saída. Ambos usavam FileChannel com lock manual. Quando o volume aumentou, começamos a ver deadlocks esporádicos. A causa não era óbvia no log, porque o deadlock ocorria apenas quando os dois workers chegavam ao final de seus chunks simultaneamente e tentavam adquirir o lock no mesmo intervalo de tempo. A solução foi implementar um buffer circulante para cada worker e fazer o flush em lote, eliminando a necessidade de sincronização fina durante a escrita. O throughput ganhou cerca de 40% sem alteração de hardware.
Insights que não aparecem em manuais
Não adianta ter dezesseis núcleos se o seu algoritmo não suporta paralelismo de dados. Algoritmos recursivos com dependência entre subprocessos, como certas formas de dinamic programming, têm limite teórico de speedup dado pela lei de Amdahl. Mesmo com hardware ilimitado, a parte sequencial domina. A primeira coisa a verificar é se o gargalo está no código, não na máquina. Outro ponto é o efeito de cache thrashing. Quando múltiplas threads acessam estruturas grandes adjacentes na memória, elas invalidam as linhas de cache umas das outras. Isso é particularmente visível em arrays bidimensionais processados por linha. A correção é usar padding ou reorganizar a disposição dos dados para que linhas de memória diferentes sejam acessadas por threads diferentes.
Limitações e quando não usar
Multiprocessamento não resolve problemas de contenção. Se você tem um recurso compartilhado, como uma conexão de banco de dados ou um lock global, adicionar threads apenas aumenta a fila de espera. Nesse caso, o melhor caminho é particionar os recursos, não aumentar a concorrência. Também não é recomendado para cargas com dependência estrita de ordem, como processamento de stream de eventos onde a sequência importa. Nesse cenário, processamento distribuído com partições ordenadas por key é mais adequado do que threads concorrentes.
Checklist prático
Meça o overhead de serialização. Identifique gargalos de E/S vs CPU. Defina pool de threads baseado em núcleos físicos, não lógicos. Use chunks entre 1 MB e 10 MB conforme o perfil. Prefira work-stealing para tarefas irregulares. Teste com dois, quatro e oito threads antes de escalar. Sempre perfile antes de otimizar.