Article image

RA

Raphael Afonso21/02/2024 19:32
Compartilhe

Como funciona a Parallel.ForEach no .NET

  • #.NET
  • #.NET C#

Frequentemente nos deparamos com a necessidade de executar tarefas repetitivas, como processar grandes volumes de dados ou realizar operações intensivas. No entanto, o dilema surge: seguir o caminho sequencial, onde as tarefas são realizadas uma após a outra, ou explorar o terreno do paralelismo, onde várias ações podem ocorrer simultaneamente.

É nesse cenário que entra a Parallel Task Library (TPL) em .NET, proporcionando uma maneira eficaz para a execução paralela de tarefas. Neste artigo, embarcaremos em uma jornada para entender como o paralelismo, a TPL e o Parallel.ForEach se entrelaçam para otimizar nossos códigos e transformar operações repetitivas em uma execução estruturada para criar resultados mais rápidos e eficazes.

Parallel.ForEach

Parallel.ForEach é uma função da TPL que simplifica a execução paralela de iterações em uma coleção. Essa abordagem facilita a tarefa de distribuir o processamento entre várias threads, melhorando o desempenho ao tirar proveito de sistemas multi-core. Você pode paralelizar a execução de operações em cada item de uma coleção, exatamente como um loop ForEach tradizional, porém permitindo que essas operações ocorram simultaneamente em threads separadas.

Um exemplo simples de como utilizar:

List<int> numeros = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Uso do Parallel.ForEach para processar os números simultaneamente
Parallel.ForEach(numeros, numero =>
{
  Console.WriteLine($"Thread {Task.CurrentId} processando o número {numero}");

  // Simulando algum processamento
  ProcessarNumero(numero);
});

Neste exemplo, a lista de números é processada simultaneamente por várias threads. Cada número é passado para a função ProcessarNumero, que simula algum processamento. A saída no console mostra a execução concorrente dos diferentes números em threads separadas, desta forma os processamentos irão ocorrer simultanemante sem a necessidade de um aguardar o outro.

Cuidados com paralelismo

Ao realizar operações paralelas o tratamento de exceções é ainda mais importante, pois uma exceção não tratada em uma thread paralela poderá resultar na terminação prematura do Parallel.ForEach ou até mesmo causar comportamentos inesperados. Sempre que possível lide com os erros dentro do proprío loop e quando não for possível utilize o AggregateException para lidar com cada erro que não foi tratado dentro do bloco de execução.

try
{
  Parallel.ForEach(collection, item =>
  {
      // Operação paralela
  });
}
catch (AggregateException ex)
{
  // Lida com exceções aqui
  foreach (var innerException in ex.InnerExceptions)
  {
      // Processa cada exceção
  }
}

Outro assunto importante em processos paralelos é o acesso atômico a variáveis compartilhadas por diferentes threads, ou seja, tome cuidado ao modificar variáveis que serão acessadas por diferentes threads simultaneas, pois elas poderão encontrar valores diferentes do esperado. Nesses casos, opte por utilizar a biblioteca System.Collections.Concurrent, Locks ou até mesmo Semáforos. Assim é possível sincronizar ou orquestrar o acesso a variável.

int total = 0;
object lockObj = new object();

Parallel.ForEach(collection, item =>
{
  // Operação paralela
  lock (lockObj)
  {
      total += AlgumaOperacao(item);
  }
});

Personalizando o comportamento

Em alguns cenários, apenas utilizar a configuração padrão do Parallel.ForEach pode não ser suficiente, então é possível recorrer ao ParallelOptions que proporcionará uma variedade de configurações que podem ser ajustadas para atender a necessidades específicas. As duas mais utilizadas são o controle sobre o número máximo de threads, ou seja, limitar a quantidade de atividades paralelas e a possibilidade de cancelamento da execução.

Controle de número máximo de Threads:

ParallelOptions parallelOptions = new ParallelOptions
{
  MaxDegreeOfParallelism = 4 // Limita o número de threads a 4
};

Parallel.ForEach(collection, parallelOptions, item =>
{
  // Operação paralela
});

No exemplo, MaxDegreeOfParallelism é definido como 4, o que significa que, no máximo, quatro threads serão usadas simultaneamente durante a execução do Parallel.ForEach. Isso permite um controle mais fino sobre a carga do sistema e pode ser ajustado de acordo com as características do ambiente de execução.

Cancelamento Assíncrono:

Outra capacidade valiosa fornecida por ParallelOptions é a habilidade de cancelar as operações paralelas de forma assíncrona, usando um CancellationToken. Isso é útil quando há a necessidade de interromper a execução em resposta a eventos externos.

// Tempo limite para o cancelamento (timeout)
TimeSpan timeout = TimeSpan.FromSeconds(5);

// Criando um CancellationTokenSource com timeout
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(timeout);
CancellationToken cancellationToken = cancellationTokenSource.Token;

ParallelOptions parallelOptions = new ParallelOptions
{
  CancellationToken = cancellationToken // Vincula o cancellationToken criado
};

Parallel.ForEach(collection, parallelOptions, item =>
{
  // Verifica se o cancelamento foi solicitado
  cancellationToken.ThrowIfCancellationRequested();
  
  // Operação paralela
});

O CancellationTokenSource é configurado com um timeout de 5 segundos (TimeSpan.FromSeconds(5)). O token associado é utilizado no Parallel.ForEach para verificar se o cancelamento foi solicitado após cada iteração. Caso o timeout seja atingido, uma exceção OperationCanceledException será lançada.

Conclusão

Ao compreender e aplicar efetivamente o paralelismo com a TPL em .NET, podemos otimizar nossos códigos, transformando operações repetitivas em execuções estruturadas, resultando em resultados mais rápidos e eficazes. O paralelismo torna-se, assim, uma ferramenta valiosa para lidar com desafios de processamento intensivo de dados em ambientes de desenvolvimento.

Este tema pode parecer muito complexo, e é. Por isso é interessante seguir o estudo de paralelismo e eu recomendo os seguintes tópicos:

  • Semáforos - São úteis para coordenar Threads e garantir que valores se "embolem" quando diferentes threads precisarem acessar a mesma variável. https://learn.microsoft.com/pt-br/dotnet/standard/threading/semaphore-and-semaphoreslim;
  • Lock - De uma maneira mais simples que os semáforos, auxiliam a que threads não manipulem ao mesmo tempo uma variável compartilhada. https://learn.microsoft.com/pt-br/dotnet/csharp/language-reference/statements/lock;
  • Collections.Concurrent - Quando for necessário usar List/Collection em um contexto de paralelismo, trás uma maneira segura de threads acessarem a mesma coleção. https://learn.microsoft.com/pt-br/dotnet/standard/collections/thread-safe/;
  • CancellationToken - Fornece uma maneira controlada de encerrar threads.https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken;
Compartilhe
Comentários (0)