Trabalhando com Parallel.Foreach
No dia a dia como desenvolvedor de software nos deparamos com cenários em que precisamos processar grandes volumes de dados. Com isso surge a grande questão: devemos trabalhar de forma sequencial? Ou paralelizar o processamento para um melhor desempenho?
Bem, nesses cenários devemos considerar o uso da TPL para trabalhar com paralelismo no .NET. Mas, nesse artigo, vamos tratar especialmente do método Parallel.ForEach.
O Parallel.ForEach é um método da TPL, biblioteca que nos auxilia na execução de atividades paralelas. Utilizando a abordagem do Parallel.ForEach conseguimos dividir o processamento em várias threads de forma simultânea que, em sua grande maioria, melhora o desempenho.
Para entender o Parallel.ForEach imagine um loop normal através de um ForEach, porém possibilitando que essas iterações ocorram de forma simultânea em threads separadas.
Observe o trecho de código abaixo para um melhor entendimento:
List<int> numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
await ParallelForeachAsync(numbers);
async Task ParallelForeachAsync(List<int> numbers)
{
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 2
};
await Parallel.ForEachAsync(numbers, options, async (number, cancellationToken) => {
await ExampleOfOperation();
Console.WriteLine($"Processing the number: {number}");
});
}
async Task ExampleOfOperation() =>
await Task.Delay(1000);
Para simplificar, vamos dividir esse código em pequenos blocos:
- Definimos uma coleção de inteiros com 10 itens.
- Temos o método ParallelForeachAsync. Nesse método implementamos o Parallel.ForEachAsync. Adentrando o método, observe que definimos um ParallelOptions informando a quantidade máxima de paralelismos das threads.
- Temos a implementação do Parallel.ForEachAsync que, por sua vez, chama o método ExampleOfOperation. Nesse método, temos apenas um delay de 1 segundo simbolizando uma operação de negócio.
- Por último, exibimos uma mensagem informando os números que foram processados de acordo com a iteração.
Observe a execução dessa implementação:
Ao definir a quantidade máxima de paralelismo em 2, observe que no console são exibidas duas mensagens por vez, observe também que a execução não ocorre na ordem em que os elementos foram adicionados à lista e sim conforme as threads forem pegando/concluindo seu processamento.
Agora, vamos testar com um paralelismo de 5 threads simultâneas, observe que tivemos um ganho de performance significativo:
Lembre-se, não existe bala de prata!
Utilize esse recurso com cuidado, além de aumentar a complexidade do código, podem surgir problemas difíceis de identificar e tratar que dificilmente você encontrará em códigos sequenciais.
Pontos de atenção
- Evite o Parallel.Foreach em loops que possuem poucas interações e seus delegados executem rapidamente.
- Evite gravar em locais de memórias compartilhados, como variáveis estáticas ou propriedades de classes, existe um potencial enorme de você enfrentar problemas de concorrência ou race condition. Mas, você pode tratar esses problemas utilizando locks para sincronizar o acesso à essas variáveis. Cuidado com essa abordagem, pois o custo da sincronização pode prejudicar a performance.
- Evite o excesso de paralelização, seus recursos são limitados. Você precisa de threads livres para processar outras demandas. Como boa prática, você pode definir o número máximo para o paralelismo.
É isso pessoal, chegamos ao fim de mais um artigo. Você pode encontrar o código desse exemplo em meu GitHub, fique à vontade para clonar e fazer seus testes.
Caso achem interessante, peço por favor uma ⭐️ no repositório apoiando. Fica também o convite para vocês me seguirem nas redes Linkedin e Instagram.
Referências
Threading.tasks.parallel.foreachasync
Potential-pitfalls-in-data-and-task-parallelism