Compartilhar via


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 System.Linq namespace 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 maneira segura de tipo. 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 de várias maneiras se assemelha a uma consulta LINQ to Objects não paralela. 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 o PLINQ tenta fazer uso total de todos os processadores no sistema. Ele faz isso particionando a fonte de dados em segmentos e 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 rapidamente.

Por meio da execução paralela, o PLINQ pode obter melhorias significativas de desempenho em relação ao código herdado para determinados tipos de consultas, geralmente 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 determinadas consultas. Portanto, você deve entender como problemas como ordenação afetam consultas paralelas. Para obter mais informações, consulte Noções básicas sobre a aceleração no PLINQ.

Observação

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 toda a funcionalidade do PLINQ. Ela e o restante dos tipos System.Linq de namespace são compilados no assembly System.Core.dll. Os projetos padrão do 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 compatíveis com LINQ to Objects, embora não tente paralelizar cada um deles. Se você não estiver familiarizado com 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 à execução paralela. Esses métodos específicos do 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 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 o 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 o PLINQ para o restante da consulta não é necessário para preservar a ordenação da sequência de origem.
WithCancellation Especifica que o PLINQ deve monitorar periodicamente o estado do token de cancelamento fornecido e cancelar a execução se ele 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 a PLINQ deve, se possível, mesclar resultados paralelos novamente em apenas uma sequência no segmento de consumo.
WithExecutionMode Especifica se o 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 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.
Aggregate sobrecarga 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 aceitação

Ao escrever uma consulta, opte pelo PLINQ invocando o ParallelEnumerable.AsParallel método de extensão 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 AsParallel método de extensão associa os operadores de consulta subsequentes, nesse 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 a consulta provavelmente produzirá acelerações por paralelização, o PLINQ particionará a sequência de origem em tarefas que podem ser executadas simultaneamente. Se não for seguro paralelizar uma consulta, o PLINQ executará a consulta sequencialmente. Se o PLINQ tiver uma opção entre um algoritmo paralelo potencialmente caro ou um algoritmo sequencial barato, ele escolherá o algoritmo sequencial por padrão. Você pode usar o WithExecutionMode método e a System.Linq.ParallelExecutionMode enumeração para instruir o PLINQ a selecionar o algoritmo paralelo. Isso é útil quando você sabe, testando e medindo que uma consulta específica é 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, o PLINQ usa todos os processadores no computador host. Você pode instruir o 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 determinada quantidade de tempo de CPU. O snippet a seguir limita a consulta a utilizar um máximo de 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 associado à computação, como E/S de Arquivo, pode ser benéfico especificar um grau de paralelismo maior que o número de núcleos no computador.

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. O PLINQ fornece o AsOrdered operador para essa finalidade. AsOrdered é diferente 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 operação paralela ordenada específica é 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 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 entregues de maneira sequencial. Os ParallelEnumerable operadores de consulta são revertidos 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 AsSequential, todos os operadores subsequentes na consulta são executados sequencialmente até AsParallel serem chamados novamente. Para obter mais informações, consulte 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, pode ser benéfico especificar um tipo específico de operação de mesclagem, por exemplo, para começar a produzir resultados mais rapidamente. Para essa finalidade, o PLINQ dá suporte ao WithMergeOptions método e à 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. 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. No PLINQ, você pode usar foreach quando deve preservar a ordem final dos resultados da consulta e também sempre que estiver processando os resultados de maneira serial, por exemplo, quando estiver chamando Console.WriteLine cada elemento. Para uma execução de consulta mais rápida quando a preservação da ordem não for necessária e quando o processamento dos resultados puder 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

O PLINQ é integrado 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 cancelável, use o WithCancellation operador na consulta e forneça uma CancellationToken instância como o 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 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 representantes do 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 geradas de threads diferentes simultaneamente. Além disso, o código para lidar com 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 obter mais informações, consulte Como lidar com exceções em uma consulta PLINQ.

Particionadores Personalizados

Em alguns casos, você pode melhorar o desempenho da consulta escrevendo um particionador personalizado que aproveita alguma característica 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 dá suporte a 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 dá suporte apenas ao 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 muita computação ou se a fonte de dados for pequena, uma consulta PLINQ poderá ser mais lenta do que uma consulta LINQ to Objects sequencial. Você pode usar o Analisador de Desempenho Paralelo no Visual Studio Team Server para comparar o desempenho de várias consultas, localizar gargalos de processamento e determinar se a consulta está em execução em paralelo ou sequencialmente. Para obter mais informações, consulte Visualizador simultâneo e como medir o desempenho da consulta PLINQ.

Consulte também