Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Em muitos casos, o PLINQ pode fornecer melhorias significativas de desempenho em relação às consultas LINQ to Objects sequenciais. No entanto, o trabalho de paralelização da execução da consulta introduz complexidade que pode levar a problemas que, em código sequencial, não são tão comuns ou não são encontrados. Este tópico lista algumas práticas a serem evitadas ao escrever consultas PLINQ.
Não assuma que o paralelo é sempre mais rápido
Às vezes, a paralelização faz com que uma consulta PLINQ seja executada mais lentamente do que seu equivalente LINQ to Objects. A regra básica é que as consultas com poucos elementos de fonte e delegações rápidas de usuários provavelmente não terão grande aceleração. No entanto, como muitos fatores estão envolvidos no desempenho, recomendamos que você meça os resultados reais antes de decidir se deseja usar o PLINQ. Para obter mais informações, consulte Compreender o Aumento de Velocidade no PLINQ.
Evite gravar em locais de memória compartilhada
Em código sequencial, não é incomum ler ou gravar em variáveis estáticas ou campos de classe. No entanto, sempre que vários threads estão a aceder a essas 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 da sincronização pode prejudicar o desempenho. Portanto, recomendamos que você evite, ou pelo menos limite, o acesso ao estado compartilhado em uma consulta PLINQ tanto quanto possível.
Evite a paralelização excessiva
Ao utilizar o AsParallel método, você incorre nos custos indiretos de particionar a coleção de origem e sincronizar os threads de execução. Os benefícios da paralelização são ainda mais limitados pelo número de processadores no computador. Não há nenhuma aceleração a ser obtida executando vários threads ligados à computação em apenas um processador. Portanto, é preciso ter cuidado para não realizar paralelização excessiva em uma consulta.
O cenário mais comum em que a paralelização excessiva pode ocorrer é em consultas aninhadas, conforme mostrado no trecho a seguir.
var q = from cust in customers.AsParallel()
from order in cust.Orders.AsParallel()
where order.OrderDate > date
select new { cust, order };
Dim q = From cust In customers.AsParallel()
From order In cust.Orders.AsParallel()
Where order.OrderDate > aDate
Select New With {cust, order}
Nesse caso, é melhor paralelizar apenas a fonte de dados externa (clientes), a menos que uma ou mais das seguintes condições se apliquem:
A fonte de dados interna (cust.Orders) é conhecida por ser muito longa.
Você está realizando um cálculo dispendioso em cada pedido. (A operação mostrada no exemplo não é cara.)
O sistema de destino é conhecido por ter processadores suficientes para lidar com o número de threads que serão produzidos paralelizando a consulta no
cust.Orders.
Em todos os casos, a melhor maneira de determinar a forma de consulta ideal é testar e medir. Para obter mais informações, consulte Como medir o desempenho da consulta PLINQ.
Evite chamadas para métodos não thread-safe
Gravar em métodos de instância não seguros para threads a partir de uma consulta PLINQ pode levar à corrupção de dados que pode ou não passar despercebida no seu programa. Pode também conduzir a exceções. No exemplo a seguir, várias threads estariam tentando chamar simultaneamente o método FileStream.Write, o que não é suportado pela classe.
Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));
Limitar chamadas a métodos thread-safe
A maioria dos métodos estáticos no .NET são thread-safe e podem ser chamados de vários threads simultaneamente. No entanto, mesmo nesses casos, a sincronização envolvida pode levar a uma lentidão significativa na consulta.
Observação
Você mesmo pode testar isso inserindo algumas chamadas para WriteLine em suas consultas. Embora este método seja usado nos exemplos de documentação para fins de demonstração, não o use em consultas PLINQ.
Evite operações de encomenda desnecessárias
Quando o PLINQ executa uma consulta em paralelo, ele divide a sequência de origem em partições que podem ser operadas simultaneamente em vários threads. Por padrão, a ordem em que as partições são processadas e os resultados são entregues não é previsível (exceto para operadores como OrderBy). Você pode instruir o PLINQ a preservar a ordenação de qualquer sequência de origem, mas isso tem um impacto negativo no desempenho. A melhor prática, sempre que possível, é estruturar as consultas de forma que não dependam da preservação da ordem. Para obter mais informações, consulte Preservação de Ordem no PLINQ.
Prefira ForAll a ForEach quando for possível
Embora o PLINQ execute uma consulta em vários threads, se consumir os resultados num foreach loop (For Each no Visual Basic), os resultados da consulta deverão ser mesclados novamente num único thread e acessados serialmente pelo enumerador. Em alguns casos, isso é inevitável; No entanto, sempre que possível, use o ForAll método para permitir que cada thread produza seus próprios resultados, por exemplo, gravando em uma coleção thread-safe, como System.Collections.Concurrent.ConcurrentBag<T>.
A mesma questão se aplica ao Parallel.ForEach. Por outras palavras, source.AsParallel().Where().ForAll(...) deve ser fortemente preferido a Parallel.ForEach(source.AsParallel().Where(), ...).
Esteja ciente dos problemas de afinidade de thread
Algumas tecnologias, por exemplo, interoperabilidade COM para componentes STA (Single-Threaded Apartment), Windows Forms e Windows Presentation Foundation (WPF), impõem restrições de afinidade de thread que exigem que o código seja executado em um thread específico. Por exemplo, no Windows Forms e no WPF, um controle só pode ser acessado no thread no qual foi criado. Se você tentar acessar o estado compartilhado de um controle Windows Forms em uma consulta PLINQ, uma exceção será gerada se você estiver executando no depurador. (Esta definição pode ser desativada.) No entanto, se a sua consulta for consumida no thread da UI, poderá aceder ao controlo a partir do loop que enumera os resultados da foreach consulta, uma vez que esse código é executado em apenas um thread.
Não assuma que as iterações de ForEach, For e ForAll sempre são executadas em paralelo
É importante ter em mente que as iterações individuais num loop Parallel.For, Parallel.ForEach ou ForAll podem ser, mas não precisam, executadas em paralelo. Portanto, você deve evitar escrever qualquer código que dependa para a correção da execução paralela de iterações ou da execução de iterações em qualquer ordem específica.
Por exemplo, é provável que este código bloqueie:
Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.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, Environment.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 aguardam o evento. Nenhuma das iterações em espera pode ser concluída até que a iteração de configuração de eventos seja concluída. No entanto, é possível que as iterações em espera bloqueiem todos os threads usados para executar o loop paralelo, antes que a iteração de configuração de eventos tenha tido a chance de ser executada. Isso resulta em um impasse – a iteração de configuração de eventos nunca será executada e as iterações em espera nunca serão ativadas.
Em particular, uma iteração de um loop paralelo nunca deve esperar por outra iteração do loop para progredir. Se o loop paralelo decidir agendar as iterações sequencialmente, mas na ordem oposta, ocorrerá um impasse.