Introdução ao PLINQ

PLINQ (LINQ) paralela é uma implementação paralela do padrão LINQ (consulta integrada à linguagem). O 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. O PLINQ combina a simplicidade e a legibilidade da sintaxe LINQ com o poder da programação paralela.

Dica

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

O que é uma consulta paralela?

Uma consulta PLINQ, em muitas formas, é semelhante a uma consulta não paralela de LINQ to Objects. As consultas PLINQ, assim como as consultas sequenciais do LINQ, operam em qualquer fonte de dados IEnumerable ou IEnumerable<T> na memória e adiam a execução, o que significa que não começam a execução até que a consulta seja enumerada. A principal diferença é que a PLINQ tenta fazer uso integral de todos os processadores no sistema. Ela faz isso particionando a fonte de dados em segmentos e então 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 de forma significativamente mais rápida.

Por meio da execução paralela, a PLINQ pode obter melhorias significativas de desempenho sobre o código herdado para determinados tipos de consultas, com frequência simplesmente adicionando a operação de consulta AsParallel à fonte de dados da operação. No entanto, o paralelismo pode apresentar suas próprias complexidades e nem todas as operações de consulta são executadas mais rapidamente na PLINQ. Na verdade, a paralelização realmente atrasa determinadas consultas. Portanto, você deve compreender como problemas como a classificação afetam consultas paralelas. Para saber mais, veja Noções básicas sobre agilização em PLINQ.

Observação

Esta documentação usa expressões lambda para definir representantes na PLINQ. Se você não estiver familiarizado com expressões lambda no C# ou no Visual Basic, veja 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 mais informações e exemplos de código.

A Classe ParallelEnumerable

A classe System.Linq.ParallelEnumerable expõe quase todas as funcionalidades de PLINQ. Ela e o restante dos tipos System.Linq de namespace são compilados no assembly System.Core.dll. Os projetos padrão de C# e do 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 com suporte do LINQ to Objects, embora ele não tente paralelizar cada uma. 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 de PLINQ são listados na tabela a seguir.

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

O Modelo de Aceite

Ao escrever uma consulta, escolha o 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("{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 método de extensão AsParallel associa os operadores de consulta subsequentes, nesse caso, where e select, às implementações System.Linq.ParallelEnumerable.

Modos de Execução

Por padrão, a PLINQ é conservadora. Em tempo de execução, a infraestrutura da PLINQ analisa a estrutura geral da consulta. Se for provável que a consulta produza aumentos de velocidade por paralelização, a PLINQ particionará a sequência de origem em tarefas que podem ser executadas simultaneamente. Se não for seguro a paralelização de uma consulta, a PLINQ apenas executa a consulta em sequência. Se a PLINQ tiver a opção de escolher entre um algoritmo paralelo potencialmente caro ou um algoritmo sequencial barato, ela escolherá o algoritmo sequencial, por padrão. Você pode usar o método WithExecutionMode e a enumeração System.Linq.ParallelExecutionMode para instruir o PLINQ a selecionar o algoritmo em paralelo. Isso é útil quando você sabe por meio de testes e medidas que uma determinada consulta é executada mais rapidamente em paralelo. Para saber mais, veja Como especificar o modo de execução em PLINQ.

Grau de paralelismo

Por padrão, o PLINQ utiliza a todos os processadores no computador host. Você pode instruir o PLINQ a não usar mais do que um número especificado de processadores usando o método WithDegreeOfParallelism. Isso é útil quando você deseja certificar-se de que outros processos em execução no computador receberão um determinado período de tempo de CPU. O snippet a seguir limita a consulta ao 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

Em casos onde uma consulta esteja executando uma quantidade significativa de trabalho associado a computação, como E/S de Arquivo, pode ser útil especificar um grau de paralelismo maior do que o número de núcleos no computador.

Consultas Paralelas Ordenadas Contra Não Ordenadas

Em algumas consultas, um operador de consulta deve produzir resultados que preservem a ordenação da sequência de origem. O PLINQ fornece o operador AsOrdered para essa finalidade. AsOrdered é diferente de AsSequential. Uma sequência AsOrdered ainda é processada em paralelo, mas os 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 padrão AsUnordered. Se uma determinada operação paralela ordenada será mais rápida do que uma versão sequencial da operação, isso dependerá de vários fatores.

O exemplo de código a seguir mostra como aceitar a preservação da ordem.

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 saber mais, veja Preservação da ordem em PLINQ.

Consultas paralelas vs. sequenciais

Algumas operações exigem que os dados de origem sejam entregue de forma sequencial. Os operadores de consulta ParallelEnumerable voltam para o modo sequencial automaticamente, quando necessário. Para os operadores de consulta definidos pelo usuário e os representantes do 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é AsParallel ser chamado novamente. Para saber mais, veja Como combinar consultas LINQ paralelas e sequenciais.

Opções para Mesclar Resultados da Consulta

Quando uma consulta PLINQ é executada em paralelo, seus resultados de cada thread de trabalho precisam ser mesclados de volta com o thread principal para serem consumidos por um loop foreach (For Each em Visual Basic) ou inseridos em uma lista ou matriz. Em alguns casos, ela pode ser útil para especificar um determinado tipo de operação de mesclagem, por exemplo, para começar a produzir resultados mais rapidamente. Para essa finalidade, o PLINQ dá suporte ao método WithMergeOptions e à enumeração ParallelMergeOptions. Para saber mais, veja Opções de mesclagem em PLINQ.

O Operador ForAll

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

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 é integrada aos tipos de cancelamento no .NET. (Para obter mais informações, consulte Cancelamento em Threads Gerenciados.) Portanto, ao contrário das consultas sequenciais LINQ to Objects, as consultas PLINQ podem ser canceladas. Para criar uma consulta PLINQ anulável, use o operador WithCancellation na consulta e forneça uma instância CancellationToken como argumento. Quando a propriedade IsCancellationRequested no token é definida como true, o PLINQ a observa, para o processamento de todos os threads e lança um OperationCanceledException.

É possível que uma consulta PLINQ possa continuar a processar alguns elementos depois que o token de cancelamento é definido.

Para maior capacidade de resposta, você também pode responder a solicitações de cancelamento em representantes do usuário de longa duração. Para saber mais, veja Como cancelar uma consulta PLINQ.

Exceções

Quando uma consulta PLINQ é executada, várias exceções podem ser geradas de diversos threads simultaneamente. Além disso, o código para tratar a exceção pode estar em um thread diferente do código que gerou a exceção. O PLINQ utiliza o tipo AggregateException para encapsular todas as exceções que foram geradas por uma consulta e para realizar marshaling nessas exceções no thread de chamada. No thread de chamada, apenas um bloco try-catch é necessário. No entanto, você pode iterar por todas as exceções encapsuladas na AggregateException e capturar qualquer uma que você possa recuperar com segurança. Em casos raros, podem ser geradas algumas exceções que não são encapsuladas em um AggregateException, e ThreadAbortExceptions também não são encapsuladas.

Quando as exceções tiverem permissão de emergirem novamente para o thread de associação, então será possível que uma consulta continue a processar alguns itens após a geração da exceção.

Para saber mais, veja Como tratar exceções em uma consulta PLINQ.

Particionadores Personalizados

Em alguns casos, você pode melhorar o desempenho da consulta escrevendo um particionador personalizado que aproveite algumas características da fonte de dados. Na consulta, o particionador personalizado em si é o objeto enumerável que será 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))

A PLINQ oferece suporte a um número fixo de partições (embora os dados possam ser dinamicamente reatribuídos a essas partições durante o tempo de execução para balanceamento de carga.). For e ForEach dão suporte somente ao particionamento dinâmico, o que significa que o número de partições é alterado em tempo de execução. Para saber mais, veja Particionadores personalizados para PLINQ e TPL.

Medindo Desempenho PLINQ

Em muitos casos, uma consulta pode ser paralelizada, mas a sobrecarga de configuração de consulta paralela supera o benefício de desempenho obtido. Se uma consulta não gerar muita computação ou se a fonte de dados for pequena, uma consulta PLINQ poderá ser mais lenta do que uma consulta sequencial LINQ to Objects. Você pode usar o Analisador de Desempenho Paralelo no Visual Studio Team Server para comparar o desempenho de várias consultas, para localizar gargalos de processamento e para determinar se a consulta está em execução em paralelo ou sequencialmente. Para saber mais, veja Visualizador de Simultaneidade e Como medir o Desempenho da Consulta PLINQ.

Confira também