Possíveis armadilhas com PLINQ

Em muitos casos, o PLINQ pode fornecer melhorias significativas no desempenho sobre seqüencial LINQ to Objects consultas de. No entanto, o trabalho de execução da consulta de paralelizar introduz complexidade pode levar a problemas que, no código seqüencial, não são tão comuns ou não são encontrados. Este tópico lista algumas práticas recomendadas para evitar quando você escrever consultas PLINQ.

Não assuma que paralelo é sempre mais rápida

Às vezes, a paralelização faz com que uma consulta PLINQ executar mais lentamente do que o seu LINQ to Objects o equivalente de. O princípio básico é que consultas que tenham alguns elementos de origem e os representantes de rápida de usuário improvável de aumento de velocidade muito. No entanto, porque muitos fatores estão envolvidos no desempenho, recomendamos que medir os resultados reais antes de decidir usar PLINQ. Para obter mais informações, consulte Aumento de velocidade de compreensão no PLINQ.

Evitar a escrita para locais de memória compartilhada

No código seqüencial, não é incomum para ler ou gravar variáveis estáticas ou campos de classe. No entanto, sempre que vários threads acessam tais variáveis simultaneamente, há um grande potencial para condições de corrida. Embora você possa usar bloqueios para sincronizar o acesso à variável, o custo de sincronização pode prejudicar o desempenho. Portanto, recomendamos que você evitar, ou pelo menos limitar, acesso ao estado em uma consulta PLINQ tanto quanto possível compartilhado.

Evite Over-Parallelization

Usando o AsParallel operador, você pagar os custos indiretos de particionamento de coleção de origem e a sincronização de threads de trabalho. Os benefícios da paralelização ainda estão limitados pelo número de processadores no computador. Não há nenhum aumento de velocidade a ser obtido através da execução de vários segmentos de limite de computação em apenas um processador. Portanto, você deve ter cuidado para não over-parallelize uma consulta.

O cenário mais comum na qual over-parallelization pode ocorrer é em consultas aninhadas, como mostrado no trecho a seguir.

        Dim q = From cust In customers.AsParallel()
                        From order In cust.Orders.AsParallel()
                        Where order.OrderDate > aDate
                        Select New With {cust, order}

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };

Nesse caso, é melhor paralelizar apenas a fonte de dados externo (clientes), a menos que uma ou mais das seguintes condições se aplicam:

  • A fonte de dados internos (cust.Pedidos) é conhecido por ser muito longo.

  • Você está executando uma computação cara em cada pedido. (A operação mostrada no exemplo não é cara).

  • O sistema de destino é conhecido por ter processadores de lidar com o número de segmentos será produzido colocando-se a consulta em cust.Orders.

Em todos os casos, a melhor maneira de determinar a forma ideal de consulta é testar e medir. Para obter mais informações, consulte Como: Medir o desempenho da consulta PLINQ.

Evitar chamadas para métodos de Thread Safe

Métodos de instância de thread safe gravar a partir de um PLINQ consulta pode levar à corrupção de dados que podem ou não detectadas em seu programa. Ele também pode causar exceções. No exemplo a seguir, vários threads é tentar chamar o Filestream.Write método simultaneamente, que não há suporte para a classe.

Dim fs As FileStream = File.OpenWrite(…)
a.Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

Chamadas de limite para os métodos de Thread-Safe.

Métodos estáticos mais na.NET Framework são thread-safe e pode ser chamado de vários threads simultaneamente. No entanto, mesmo nesses casos, a sincronização envolvida pode levar a redução significativa na consulta.

Observação

Você pode testar isso sozinho, inserindo algumas chamadas para WriteLine em consultas.Embora este método é usado nos exemplos de documentação para fins de demonstração, não o use em consultas PLINQ.

Evitar operações de ordenação desnecessários

Quando o PLINQ executa uma consulta em paralelo, ele divide a seqüência de origem em partições que podem ser operadas em simultaneamente em vários segmentos. Por padrão, a ordem na qual as partições são processadas e os resultados são entregues não é previsível (exceto para os operadores, como OrderBy). Você pode instruir o PLINQ para preservar a ordenação de qualquer seqüência de origem, mas isso tem um impacto negativo no desempenho. A prática recomendada, sempre que possível, é estruturar consultas para que eles não dependem de preservação da ordem. Para obter mais informações, consulte Preservação da ordem PLINQ.

Prefira ForAll ForEach quando é possível

Embora o PLINQ executa uma consulta em vários segmentos, se você consumir os resultados em um foreach loop (For Each em Visual Basic), e em seguida, os resultados da consulta devem ser mesclados novamente em um thread e serialmente acessados pelo enumerador. Em alguns casos, isso é inevitável; No entanto, sempre que possível, use o ForAll método para ativar cada thread de saída de seus próprios resultados, por exemplo, escrevendo-se a uma coleção de thread-safe, como ConcurrentBag.

O mesmo problema aplica-se a ForEach(). Em outras palavras, source.AsParallel().Where().ForAll(...) deve ser fortemente preferencial para

Parallel.ForEach(source.AsParallel().Where(), ...).

Estar ciente das questões de afinidade de Thread

Algumas tecnologias, por exemplo, a interoperabilidade COM componentes de Single-Threaded Apartment (STA), Windows Forms e Windows Presentation Foundation (WPF), impõem restrições de afinidade de thread que exigem o código para executar em um segmento específico. Por exemplo, no Windows Forms e WPF, um controle só pode ser acessado no thread no qual ele foi criado. Se você tentar acessar o estado compartilhado de um controle Windows Forms em uma consulta PLINQ, uma exceção se você estiver executando no depurador. (Essa configuração pode ter sido desativado). No entanto, se sua consulta é consumida no thread da interface do usuário, em seguida, você pode acessar o controle a partir de foreach resulta de loop que enumera a consulta, pois esse código é executado em apenas um thread.

Não assuma que iterações de ForEach, para e ForAll sempre executar em paralelo

É importante ter em mente que iterações individuais em um For(), ForEach() ou ForAll() maio de loop, mas não tem que executar em paralelo. Portanto, evite escrever qualquer código que depende de correção na execução paralela de iterações ou na execução de iterações em uma ordem específica.

Por exemplo, este código é provável que ocorra um deadlock:

        Dim mre = New ManualResetEventSlim()
            Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll(Sub(j) 

                                                             If j = Environment.ProcessorCount Then

                                                                 Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
                                                                 mre.Set()

                                                             Else

                                                                 Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
                                                                 mre.Wait()
                                                             End If
            End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
            Enumerable.Range(0, ProcessorCount * 100).AsParallel().ForAll((j) =>
            {
                if (j == Environment.ProcessorCount)
                {
                    Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
                    mre.Set();
                }
                else
                {
                    Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
                    mre.Wait();
                }
            }); //deadlocks

Neste exemplo, uma iteração define um evento e todas as outras iterações esperar o evento. Nenhuma das iterações espera pode concluir até concluir a configuração de evento iteração. No entanto, é possível que as iterações espera bloqueiam todos os threads que são usados para executar loop paralelo, antes da iteração de definição do evento teve a oportunidade de executar. Isso resulta em um deadlock – a iteração de definição do evento nunca será executado e as iterações espera nunca acordar.

Em particular, uma iteração de loop paralelo nunca deve aguardar outra iteração do loop para tornar o progresso. Se o loop paralelo decidir agendar as iterações seqüencialmente, mas na ordem oposta, ocorrerá um deadlock.

Consulte também

Conceitos

Parallel LINQ PLINQ)