C# - PLINQ - Principais métodos

 Hoje vamos rever os conceitos básicos da programação paralela na linguagem C# apresentando os principais métodos da PLINQ.

A PLINQ é a versão paralela da LINQ, e esta disponível a partir da versão 4.0 da plataforma .NET. Ela tem a capacidade de realizar consultas usando a computação paralela, e, dessa forma podemos realizar uma tarefa com PLINQ de forma que ela seja executada concorrentemente.

Para escalar as consultas LINQ para um ambiente com suporte a multiprocessadores a plataforma .NET oferece a PLINQ. E para isso a API da PLINQ é fornecida como uma paridade com a API da LINQ.

Na tabela abaixo vemos a a correspondência entre os namespaces usados na LINQ e os equivalentes da PLINQ :

LINQ

PLINQ

System.Collections.IEnumerable

System.Linq.ParallelQuery

System.Collections.Generic.IEnumerable<T>

System.Linq.ParallelQuery<T>

System.Linq.IOrderedEnumerable<T>

System.Linq.OrderedParallelQuery<T>

System.Linq.Enumerable

System.Linq.ParallelEnumerable

Observe que a classe ParallelEnumerable expõe as principais funcionalidades do PLINQ e inclui implementações de todos os operadores de consulta padrão que o LINQ suporta.

A seguir veremos alguns dos principais métodos de extensão do classe ParallelEnumerable usados para execução paralela destacando os seguintes:

Vamos iniciar como o método AsParallel() e AsOrdered().

Usando AsParallel() e AsOrdered()

O método de extensão AsParallel é o ponto de entrada para o PLINQ e especifica que a consulta deve ser processada em paralelo;  ele divide o trabalho em cada processador ou núcleo do processador.

Vejamos um exemplo de como usar este método :

using System;
using System.Collections.Generic;
using System.Linq;

namespace CPLINQ2
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            IEnumerable<Aluno> alunos = Aluno.GetAlunos();
            Console.WriteLine("Pressione algo para iniciar...");
            Console.Read();

            var resultado = from a in alunos.AsParallel()
                                     where a.Nome.StartsWith("C")
                                     select a;

            foreach (var aluno in resultado)
            {
                Console.WriteLine($"{aluno.Nome} - {aluno.Idade}");           
            }
            Console.ReadKey();
        }
    }

    internal class Aluno
    {
        public string Nome { get; set; }
        public int Idade { get; set; }
        public static IEnumerable<Aluno> GetAlunos()
        {
            var alunos = new List<Aluno> {
               new Aluno{Nome="Paulo", Idade=45},
               new Aluno{Nome="Carina", Idade=34},
               new Aluno{Nome="Danilo", Idade=28},
               new Aluno{Nome="Carlos", Idade=23},
               new Aluno{Nome="Maria", Idade=38},
               new Aluno{Nome="Catia", Idade=47},
               new Aluno{Nome="Marta", Idade=52},
               new Aluno{Nome="Clarisse", Idade=62},
               new Aluno{Nome="Claudio", Idade=23},
               new Aluno{Nome="Milena", Idade=21},
            };
            return alunos;
        }
    }

}

Temos aqui uma consulta LINQ normal onde temos uma fonte de dados do tipo Enumerable que retorna uma lista de alunos
com nome e idade :

 var resultado = from a in alunos
                                     where a.Nome.StartsWith("C")
                                     select a;

Para tornar essa consulta uma consulta PLINQ incluímos o método AsParallel na fonte de dados  e isso faz com que as iterações sejam espalhadas pelos processadores.

 var resultado = from a in alunos.AsParallel()
                                     where a.Nome.StartsWith("C")
                                     select a;

Executando veremos que a ordem da sequência de origem não é preservada  como mostra a figura :



Isso ocorre pois na PLINQ, o objetivo é maximizar o desempenho, e,  assim  uma consulta deve ser executada o mais rápido possível, e produzir os resultados corretos, mas como a ordenação pode ser uma tarefa que pode ter um grande custo computacional, por padrão, a PLINQ não preserva a ordem da sequência de origem.

Para preservar a ordem podemos usar o método AsOrdered() :

 var resultado = from a in alunos.AsParallel().AsOrdered()
                                     where a.Nome.StartsWith("C")
                                     select a;


O resultado será a sequência sendo exibida conforme a ordem original:



O desempenho da PLINQ


Agora nem sempre tornar uma consulta normal para uma consulta que executa em paralelo vai te dar um ganho de desempenho
.

Na verdade o uso do método AsParallel() não apresenta grandes resultados em todos os tipos de consultas. Para pequenas coleções ele é mais lento que a consulta linq normal.

Vejamos um exemplo onde temos una consulta linq normal que soma os números inteiro de 0 até 327267.

A seguir vamos criar outra consulta igual a essa transformar essa consulta linq em uma consulta Plinq incluindo o método AsParallel  na fonte de dados e vamos comparar o resultado:

using System;
using System.Diagnostics;
using System.Linq;
namespace CPLINQ1
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] numeros = Enumerable.Range(0, short.MaxValue).ToArray();
            Console.WriteLine("Pressione algo para iniciar...");
            Console.Read();
            Stopwatch sw = new Stopwatch();
            sw.Start();
            var resultado_Normal = numeros.Sum();
            Console.WriteLine($"Soma = {resultado_Normal}");
            sw.Stop();
            Console.WriteLine($"Processamento Normal   = {sw.Elapsed}");
            sw.Start();
            var resultado_Paralelo = numeros.AsParallel().Sum();
            Console.WriteLine($"Soma = {resultado_Paralelo}");
            sw.Stop();
            Console.WriteLine($"Processamento Paralelo = {sw.Elapsed}");
            Console.ReadKey();
        }
    }
}

Ao executarmos este código veremos que a consulta Plinq vai demorar mais de 100 vezes que a consulta normal mostrando assim que o custo do paralelismo somente é válido para consultas complexas onde grandes quantidade de dados exigem um processamento mais pesado.

O Grau de Paralelismo

Um conceito importante relacionado com a programação paralela e com o PLINQ é o grau de paralelismo.

O grau de paralelismo é um número inteiro sem sinal que indica o número máximo de processadores que sua consulta PLINQ deve aproveitar em execução.  Isso representa o número máximo de tarefas que seriam executadas simultaneamente para processar a consulta.

O valor padrão do grau de paralelismo nas consultas PLINQ é 64 o que implica que a PLINQ pode usar no máximo 64 processadores em seus sistema.

Agora, podemos alterar esse valor usando o método WithDegreeOfParallelism().

Usando como exemplo a consulta do primeiro exemplo teremos:

var resultado = from a in alunos.AsParallel().AsOrdered().WithDegreeOfParallelism(4)
                           where a.Nome.StartsWith("C")
                           select a;

Neste exemplo estamos limitando o grau de paralelismo na consulta PLINQ a 4 processadores onde o numero de processadores
foi passado como um argumento para o método WithDegreeofParallelism.

Em outro artigo veremos os demais métodos da PLINQ.

Pegue o projeto aqui :  CPLINQ1.zip

"Voz do que clama no deserto: Preparai o caminho do Senhor; endireitai no ermo vereda a nosso Deus.
Todo o vale será exaltado, e todo o monte e todo o outeiro será abatido; e o que é torcido se endireitará, e o que é áspero se aplainará."
Isaías 40:3,4

Referências:


José Carlos Macoratti