1. Introdução
Hoje vamos ver como processar grandes volumes de dados o mais rápido possível, usando todos os núcleos disponíveis do seu computador (ou servidor). Para isso vamos usar classes do namespace System.Threading.Tasks.Parallel, em particular os métodos Parallel.For e Parallel.ForEach.
E se a tarefa for puramente CPU-bound?
Um loop clássico for ou foreach processa elementos um por vez. Simples e confiável. Mas se você tem um processador com múltiplos núcleos, o loop usa só um núcleo, e os outros ficam meio ociosos. Por que não distribuir partes do array entre os núcleos para processamento simultâneo?
Exemplo:
// Vamos calcular a soma dos quadrados de 1 até N
long sum = 0;
for (int i = 1; i <= 1_000_000; i++)
{
sum += i * i;
}
Esse código é simples, mas funciona de forma sequencial. E se a gente espalhar as tarefas pelos núcleos?
Conhecendo a família: Parallel.For e Parallel.ForEach
O que são?
- Parallel.For — funciona como um loop for normal, só que divide o trabalho em partes e distribui automaticamente entre threads, usando todos os núcleos disponíveis.
- Parallel.ForEach — processa uma coleção como um foreach normal, mas também em paralelo.
Documentação oficial:
Por que é prático?
Você não precisa criar, iniciar e gerenciar threads manualmente. O framework faz a parte pesada por você. Você escreve código parecido com um loop normal, e o paralelismo acontece automaticamente por baixo dos panos.
2. Sintaxe: exemplos básicos
Parallel.For
long total = 0;
Parallel.For(1, 1_000_001, i =>
{
// Essa lambda pode ser executada simultaneamente por várias threads
Interlocked.Add(ref total, i * i); // Pra evitar race conditions
});
Console.WriteLine($"Soma dos quadrados: {total}");
Repare: a variável total é atualizada via Interlocked.Add — assim evitamos race conditions.
Parallel.ForEach
var numbers = Enumerable.Range(1, 10_000_000).ToArray();
long sum = 0;
Parallel.ForEach(numbers, num =>
{
Interlocked.Add(ref sum, num * num); // Soma segura
});
Console.WriteLine($"Soma dos quadrados: {sum}");
Olhar por dentro (esquema visual)
+-------------------+
|Coleção/faixa |
+---------+---------+
|
v
+----------------------+
| Parallel.ForEach |
+----------+-----------+
|
+----+----+----+----+
| | |
v v v
Task #1 Task #2 Task #3 ... (núcleos disponíveis)
| | |
+--+----+ +--+-----+ +--+-----+
|Processo| |Processo| |Processo|
+-------+ +--------+ +--------+
\ | /
+--------+--------+
|
v
Resultado
3. Analisando arquivos grandes (processamento CPU-bound)
Suponha que temos um arquivo de texto com dezenas de milhares de linhas — por exemplo, cada linha contém um número. Precisamos ler o arquivo, elevar cada número ao quadrado e somar os quadrados.
Versão síncrona
string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;
foreach (var line in lines)
{
if (long.TryParse(line, out long n))
{
sum += n * n;
}
}
Console.WriteLine($"Soma dos quadrados: {sum}");
Versão paralela com Parallel.For
string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;
Parallel.For(0, lines.Length, i =>
{
if (long.TryParse(lines[i], out long n))
{
Interlocked.Add(ref sum, n * n);
}
});
Console.WriteLine($"Soma dos quadrados: {sum}");
O que mudou: substituímos o loop normal por um paralelo, e sum agora é incrementada via Interlocked.Add — pra evitar conflitos entre threads.
4. O que acontece por baixo dos panos?
Quando você chama Parallel.For ou Parallel.ForEach, o .NET divide automaticamente seu trabalho em fragmentos e os distribui pelos núcleos disponíveis, usando o thread pool. Cada fragmento é processado de forma independente em sua própria thread.
Vantagem: se você tem 4 núcleos, o trabalho pode ficar quase 4 vezes mais rápido (se a tarefa não depender de recursos externos e não for limitada por outras restrições, como memória ou velocidade de leitura do disco).
Comparando tempos de execução
var numbers = Enumerable.Range(1, 100_000_000).ToArray();
long sumSync = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();
foreach (var n in numbers)
sumSync += n * n;
sw.Stop();
Console.WriteLine($"Sync: {sw.ElapsedMilliseconds} ms, soma: {sumSync}");
long sumParallel = 0;
sw.Restart();
Parallel.ForEach(numbers, n =>
Interlocked.Add(ref sumParallel, n * n)
);
sw.Stop();
Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds} ms, soma: {sumParallel}");
Experimente por conta própria! Em uma máquina poderosa o ganho pode ser várias vezes maior, mas tudo depende da tarefa e dos gargalos.
5. Dicas úteis
Controlando o grau de paralelismo
Às vezes faz sentido limitar o número de threads usados (por exemplo, pra não sobrecarregar o sistema). Use MaxDegreeOfParallelism pra isso:
using System.Threading.Tasks;
long sum = 0;
var options = new ParallelOptions {
MaxDegreeOfParallelism = 2
};
Parallel.For(0, 100, options, i =>
{
Interlocked.Add(ref sum, i * i);
});
Console.WriteLine($"Soma dos quadrados: {sum}");
Quando isso é útil: se você sabe que parte dos cálculos sobrecarrega o disco e não o processador — defina menos threads e avalie o impacto na performance.
Quando usar loops paralelos
| Loop for normal | Parallel.For/Parallel.ForEach | |
|---|---|---|
| Processadores | Usa um núcleo | Usa todos os núcleos |
| Ordem | Garantida | Não garantida |
| Velocidade | Normalmente mais lento | Freqüentemente bem mais rápido |
| Simplicidade | Muito simples | Requer atenção à thread-safety |
| Melhor uso | Pequenos volumes, I/O-bound | Grandes volumes, CPU-bound |
Extensão: o que mais o Parallel faz?
Parallel.Invoke() — executa vários métodos independentes ao mesmo tempo:
static void DoTask1() => Console.WriteLine("Tarefa 1 concluída");
static void DoTask2() => Console.WriteLine("Tarefa 2 concluída");
static void DoTask3() => Console.WriteLine("Tarefa 3 concluída");
Parallel.Invoke(
() => DoTask1(),
() => DoTask2(),
() => DoTask3()
);
Cada método será executado em seu próprio núcleo quando possível.
Aplicações na vida real
- Processamento de imagens: processar blocos diferentes simultaneamente (por exemplo, aplicar um filtro).
- Cálculos sobre arrays: cálculos financeiros, simulações (avaliação de portfólios por cenários).
- Trabalhar com grandes logs: busca e agregação usando vários núcleos.
- Machine learning: dividir em tarefas independentes (batches de dados, feature engineering).
E, claro, em entrevistas você não só vai explicar o que são loops paralelos, mas também comentar honestamente suas vantagens e desvantagens.
6. Erros típicos ao usar Parallel.For e Parallel.ForEach
Erro #1: Ignorar race conditions.
Atualizar uma variável compartilhada sem Interlocked ou lock resulta em resultados incorretos por causa do acesso concorrente das threads.
Erro #2: Usar para tarefas I/O-bound.
Loops paralelos não aceleram tarefas dependentes de disco ou rede, e podem até desacelerar devido ao overhead.
Erro #3: Presumir ordem de execução.
Loops paralelos não garantem a ordem de processamento dos elementos, o que pode quebrar a lógica se ela depender de sequência.
Erro #4: Ignorar efeitos colaterais.
Modificar estado compartilhado (por exemplo, coleções) em loops paralelos pode causar erros se não usar estruturas thread-safe.
GO TO FULL VERSION