Introdução ao PLINQ

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

Gorjeta

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> matrizes e matrizes. Este artigo pressupõe que você tenha uma compreensão básica do LINQ. Para obter mais informações, consulte Consulta integrada à linguagem (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 fonte de dados ou IEnumerable<T> na memória IEnumerable 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 AsParallel operação de consulta à 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 Understanding Speedup in PLINQ.

Nota

Esta documentação usa expressões lambda para definir delegados no PLINQ. Se você não estiver familiarizado com expressões lambda em C# ou Visual Basic, consulte 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 System.Linq.ParallelEnumerable classe expõe quase todas as funcionalidades do PLINQ. Ele e o System.Linq restante dos tipos de namespace 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 ParallelEnumerable classe 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 Description
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 deve, se possível, mesclar resultados paralelos de volta em apenas uma 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 multithreaded que, ao contrário da iteração sobre os resultados da consulta, permite que os resultados sejam processados em paralelo sem primeiro mesclar de volta ao thread 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 ParallelEnumerable.AsParallel método extension 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("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// 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 AsParallel método extension vincula os operadores de consulta subsequentes, neste caso, where e select, às System.Linq.ParallelEnumerable implementações.

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 WithExecutionMode método e a System.Linq.ParallelExecutionMode enumeração 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 WithDegreeOfParallelism método. 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 AsOrdered operador para este fim. AsOrdered é distinta de AsSequential. Uma AsOrdered sequência 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 AsOrdered sequência pode ser processada mais lentamente do que a sequência padrão AsUnordered . 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 pedidos no PLINQ.

Consultas paralelas vs. sequenciais

Algumas operações exigem que os dados de origem sejam entregues de forma sequencial. Os ParallelEnumerable operadores de consulta 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 AsSequential método. Quando você usa AsSequentialo , todos os operadores subsequentes na consulta são executados sequencialmente até AsParallel que 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, seus resultados de cada thread de trabalho devem ser mesclados de volta ao thread principal para consumo por um foreach loop (For Each no Visual Basic) ou inserção 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 WithMergeOptions método e a ParallelMergeOptions enumeração. Para obter mais informações, consulte Opções de mesclagem no PLINQ.

O operador ForAll

Em consultas LINQ sequenciais, a execução é adiada até que a consulta seja enumerada em um foreach loop (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 ele 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 ordem 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 ForAll método para executar uma consulta PLINQ. ForAll não executa esta etapa de mesclagem final. 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 WithCancellation operador na consulta e forneça uma CancellationToken instância como argumento. Quando a IsCancellationRequested propriedade no token estiver definida como true, o PLINQ notará isso, interromperá o processamento em todos os threads e lançará um OperationCanceledExceptionarquivo .

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

Para maior capacidade de resposta, você também pode responder a solicitações de cancelamento em delegados de usuário 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 de diferentes threads simultaneamente. 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 AggregateException tipo para encapsular todas as exceções que foram lançadas por uma consulta e empacotar essas exceções de volta para o thread de chamada. No thread de chamada, apenas um bloco try-catch é necessário. No entanto, você pode iterar através de todas as exceções que estão encapsuladas no AggregateException e pegar qualquer um que você pode recuperar com segurança. Em casos raros, algumas exceções podem ser lançadas que não estão embrulhadas em um AggregateException, e ThreadAbortExceptions também não são embrulhadas.

Quando as exceções são permitidas para borbulhar de volta para o thread de junção, então é possível que uma consulta pode continuar a processar alguns itens depois que 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 suporta 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 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.

Consulte também