Partilhar via


Introdução ao PLINQ

Parallel LINQ (PLINQ) é uma implementação paralela do padrão Language-Integrated Query (LINQ). PLINQ implementa o conjunto completo de operadores de consulta padrão LINQ como métodos de extensão para o namespace System.Linq e tem operadores adicionais para operações paralelas. PLINQ combina a simplicidade e legibilidade da sintaxe LINQ com o poder da programação paralela.

Sugestão

Se você não estiver familiarizado com o LINQ, ele apresenta um modelo unificado para consultar qualquer fonte de dados enumerável de maneira segura para tipos. LINQ to Objects é o nome para consultas LINQ que são executadas em coleções na memória, como List<T> e matrizes. Este artigo pressupõe que você tenha uma compreensão básica do LINQ. Para obter mais informações, consulte Language-Integrated Query (LINQ).

O que é uma consulta paralela?

Uma consulta PLINQ em muitos aspetos se assemelha a uma consulta LINQ to Objects não paralela. As consultas PLINQ, assim como as consultas LINQ sequenciais, operam em qualquer IEnumerable na memória ou IEnumerable<T> fonte de dados e têm execução adiada, o que significa que elas não começam a ser executadas até que a consulta seja enumerada. A principal diferença é que PLINQ tenta fazer pleno uso de todos os processadores no sistema. Ele faz isso particionando a fonte de dados em segmentos e, em seguida, executando a consulta em cada segmento em threads de trabalho separados em paralelo em vários processadores. Em muitos casos, a execução paralela significa que a consulta é executada significativamente mais rápido.

Através da execução paralela, o PLINQ pode alcançar melhorias significativas de desempenho em relação ao código herdado para certos tipos de consultas, muitas vezes apenas adicionando a operação de consulta AsParallel à fonte de dados. No entanto, o paralelismo pode introduzir suas próprias complexidades, e nem todas as operações de consulta são executadas mais rapidamente no PLINQ. Na verdade, a paralelização na verdade retarda certas consultas. Portanto, você deve entender como problemas como ordenação afetam consultas paralelas. Para obter mais informações, consulte Compreender o Aumento de Velocidade no PLINQ.

Observação

Esta documentação usa expressões lambda para definir delegados no PLINQ. Se não estiveres familiarizado com expressões lambda em C# ou Visual Basic, consulta expressões lambda em PLINQ e TPL.

O restante deste artigo fornece uma visão geral das principais classes PLINQ e discute como criar consultas PLINQ. Cada seção contém links para informações mais detalhadas e exemplos de código.

A classe ParallelEnumerable

A classe System.Linq.ParallelEnumerable expõe quase todas as funcionalidades do PLINQ. Ele e o restante dos tipos de namespace System.Linq são compilados no assembly System.Core.dll. Os projetos padrão C# e Visual Basic no Visual Studio fazem referência ao assembly e importam o namespace.

ParallelEnumerable inclui implementações de todos os operadores de consulta padrão suportados pelo LINQ to Objects, embora não tente paralelizar cada um deles. Se você não estiver familiarizado com o LINQ, consulte Introdução ao LINQ (C#) e Introdução ao LINQ (Visual Basic).

Além dos operadores de consulta padrão, a classe ParallelEnumerable contém um conjunto de métodos que permitem comportamentos específicos para execução paralela. Esses métodos específicos do PLINQ estão listados na tabela a seguir.

Operador ParallelEnumerable Descrição
AsParallel O ponto de entrada para PLINQ. Especifica que o restante da consulta deve ser paralelizado, se possível.
AsSequential Especifica que o restante da consulta deve ser executado sequencialmente, como uma consulta LINQ não paralela.
AsOrdered Especifica que PLINQ deve preservar a ordenação da sequência de origem para o resto da consulta, ou até que a ordem seja alterada, por exemplo, pelo uso de uma cláusula orderby (Order By no Visual Basic).
AsUnordered Especifica que o PLINQ para o restante da consulta não é necessário para preservar a ordem da sequência de origem.
WithCancellation Especifica que PLINQ deve monitorar periodicamente o estado do token de cancelamento fornecido e cancelar a execução se for solicitado.
WithDegreeOfParallelism Especifica o número máximo de processadores que o PLINQ deve usar para paralelizar a consulta.
WithMergeOptions Fornece uma dica sobre como o PLINQ pode, se possível, unir novamente os resultados paralelos numa única sequência no thread de consumo.
WithExecutionMode Especifica se PLINQ deve paralelizar a consulta, mesmo quando o comportamento padrão seria executá-la sequencialmente.
ForAll Um método de enumeração multithread que, ao contrário da iteração sobre os resultados da consulta, permite que os resultados sejam processados em paralelo sem primeiro fazer a junção de volta ao fluxo de execução do consumidor.
Aggregate sobrecarga Uma sobrecarga que é exclusiva do PLINQ e permite a agregação intermediária sobre partições thread-local, além de uma função de agregação final para combinar os resultados de todas as partições.

O modelo de aceitação

Ao escrever uma consulta, opte pelo PLINQ invocando o método de extensão ParallelEnumerable.AsParallel na fonte de dados, conforme mostrado no exemplo a seguir.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine($"{evenNums.Count()} even numbers out of {source.Count()} total");
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

O método de extensão AsParallel vincula os operadores de consulta subsequentes, neste caso, where e select, às implementações System.Linq.ParallelEnumerable.

Modos de execução

Por padrão, o PLINQ é conservador. Em tempo de execução, a infraestrutura PLINQ analisa a estrutura geral da consulta. Se é provável que a consulta produza acelerações por paralelização, o PLINQ particiona a sequência de origem em tarefas que podem ser executadas simultaneamente. Se não for seguro paralelizar uma consulta, o PLINQ apenas executa a consulta sequencialmente. Se PLINQ tem uma escolha entre um algoritmo paralelo potencialmente caro ou um algoritmo sequencial barato, ele escolhe o algoritmo sequencial por padrão. Você pode usar o método WithExecutionMode e a enumeração System.Linq.ParallelExecutionMode para instruir PLINQ a selecionar o algoritmo paralelo. Isso é útil quando você sabe, testando e medindo, que uma determinada consulta é executada mais rapidamente em paralelo. Para obter mais informações, consulte Como especificar o modo de execução no PLINQ.

Grau de paralelismo

Por padrão, PLINQ usa todos os processadores no computador host. Você pode instruir PLINQ a usar não mais do que um número especificado de processadores usando o método WithDegreeOfParallelism. Isso é útil quando você deseja garantir que outros processos em execução no computador recebam uma certa quantidade de tempo de CPU. O trecho a seguir limita a consulta a utilizar no máximo dois processadores.

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;
Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item

Nos casos em que uma consulta está executando uma quantidade significativa de trabalho não vinculado à computação, como E/S de arquivo, pode ser benéfico especificar um grau de paralelismo maior do que o número de núcleos na máquina.

Consultas paralelas ordenadas versus não ordenadas

Em algumas consultas, um operador de consulta deve produzir resultados que preservem a ordenação da sequência de origem. PLINQ fornece o operador AsOrdered para este fim. AsOrdered é distinto de AsSequential. Uma sequência AsOrdered ainda é processada em paralelo, mas seus resultados são armazenados em buffer e classificados. Como a preservação da ordem normalmente envolve trabalho extra, uma sequência AsOrdered pode ser processada mais lentamente do que a sequência AsUnordered padrão. Se uma determinada operação paralela ordenada é mais rápida do que uma versão sequencial da operação depende de muitos fatores.

O exemplo de código a seguir mostra como optar pela preservação de pedidos.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


Para obter mais informações, consulte Preservação de Ordem no PLINQ.

Consultas paralelas vs. sequenciais

Algumas operações exigem que os dados de origem sejam entregues de forma sequencial. Os operadores de consulta ParallelEnumerable revertem para o modo sequencial automaticamente quando é necessário. Para operadores de consulta definidos pelo usuário e delegados de usuário que exigem execução sequencial, o PLINQ fornece o método AsSequential. Quando você usa AsSequential, todos os operadores subsequentes na consulta são executados sequencialmente até que AsParallel seja chamado novamente. Para obter mais informações, consulte Como combinar consultas LINQ paralelas e sequenciais.

Opções para mesclar resultados de consulta

Quando uma consulta PLINQ é executada em paralelo, os seus resultados de cada thread trabalhador devem ser mesclados de volta para o thread principal para serem consumidos por um loop foreach (For Each no Visual Basic) ou inseridos em uma lista ou matriz. Em alguns casos, pode ser benéfico especificar um tipo específico de operação de fusão, por exemplo, para começar a produzir resultados mais rapidamente. Para este efeito, PLINQ suporta o método WithMergeOptions e a enumeração ParallelMergeOptions. Para obter mais informações, consulte Opções de fusão no PLINQ.

O operador "ForAll"

Em consultas LINQ sequenciais, a execução é adiada até que a consulta seja enumerada em um loop foreach (For Each no Visual Basic) ou invocando um método como ToList , ToArray ou ToDictionary. No PLINQ, você também pode usar foreach para executar a consulta e iterar através dos resultados. No entanto, foreach em si não é executado em paralelo e, portanto, requer que a saída de todas as tarefas paralelas seja mesclada de volta ao thread no qual o loop está sendo executado. No PLINQ, você pode usar foreach quando você deve preservar a ordenação final dos resultados da consulta, e também sempre que você estiver processando os resultados de forma serial, por exemplo, quando você estiver chamando Console.WriteLine para cada elemento. Para uma execução de consulta mais rápida quando a preservação da ordem não é necessária e quando o processamento dos resultados pode ser paralelizado, use o método ForAll para executar uma consulta PLINQ. ForAll não executa essa etapa final de mesclagem. O exemplo de código a seguir mostra como usar o ForAll método. System.Collections.Concurrent.ConcurrentBag<T> é usado aqui porque é otimizado para vários threads adicionando simultaneamente sem tentar remover nenhum item.

var nums = Enumerable.Range(10, 10000);
var query =
    from num in nums.AsParallel()
    where num % 10 == 0
    select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));
Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
            Where num Mod 10 = 0
            Select num

' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))

A ilustração a seguir mostra a diferença entre foreach e ForAll em relação à execução da consulta.

ForAll vs. ForEach

Cancelamento

PLINQ é integrado com os tipos de cancelamento em .NET. (Para obter mais informações, consulte Cancelamento em threads gerenciados.) Portanto, ao contrário das consultas LINQ to Objects sequenciais, as consultas PLINQ podem ser canceladas. Para criar uma consulta PLINQ cancelável, use o operador WithCancellation na consulta e forneça uma instância CancellationToken como argumento. Quando a propriedade IsCancellationRequested no token estiver definida como true, o PLINQ notará isso, interromperá o processamento em todos os threads e lançará uma OperationCanceledException.

É possível que uma consulta PLINQ continue a processar alguns elementos depois que o token de cancelamento for definido.

Para maior sensibilidade, pode-se também responder a pedidos de cancelamento em delegações de utilizador de longa duração. Para obter mais informações, consulte Como cancelar uma consulta PLINQ.

Exceções

Quando uma consulta PLINQ é executada, várias exceções podem ser lançadas simultaneamente a partir de diferentes threads. Além disso, o código para manipular a exceção pode estar em um thread diferente do código que lançou a exceção. PLINQ usa o tipo AggregateException para encapsular todas as exceções que foram lançadas por uma consulta e repassar essas exceções de volta para a thread que iniciou a chamada. No thread de chamada, apenas um bloco try-catch é necessário. No entanto, podes iterar através de todas as exceções que estão encapsuladas no AggregateException e capturar qualquer uma da qual possas recuperar-te com segurança. Em casos raros, algumas exceções podem ser levantadas que não estão envolvidas num AggregateException, e ThreadAbortExceptions também não são envolvidas.

Quando as exceções são permitidas a propagar-se de volta para o thread de união, então é possível que uma consulta possa continuar a processar alguns itens quando a exceção é gerada.

Para obter mais informações, consulte Como manipular exceções em uma consulta PLINQ.

Particionadores personalizados

Em alguns casos, você pode melhorar o desempenho da consulta escrevendo um particionador personalizado que aproveita algumas características dos dados de origem. Na consulta, o particionador personalizado em si é o objeto enumerável que é consultado.

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

O PLINQ suporta um número fixo de partições (embora os dados possam ser reatribuídos dinamicamente a essas partições durante o tempo de execução para balanceamento de carga). For e ForEach suportam apenas particionamento dinâmico, o que significa que o número de partições muda em tempo de execução. Para obter mais informações, consulte Os Particionadores Personalizados para PLINQ e TPL.

Medindo o desempenho do PLINQ

Em muitos casos, uma consulta pode ser paralelizada, mas a sobrecarga de configurar a consulta paralela supera o benefício de desempenho obtido. Se uma consulta não executar muitos cálculos ou se a fonte de dados for pequena, uma consulta PLINQ pode ser mais lenta do que uma consulta LINQ to Objects sequencial. Você pode usar o Parallel Performance Analyzer no Visual Studio Team Server para comparar o desempenho de várias consultas, localizar gargalos de processamento e determinar se sua consulta está sendo executada em paralelo ou sequencialmente. Para obter mais informações, consulte Visualizador de Simultaneidade e Como medir o desempenho da consulta PLINQ.

Ver também