CodeGym /Cursos /C# SELF /Processamento paralelo de dados

Processamento paralelo de dados

C# SELF
Nível 60 , Lição 3
Disponível

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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION