Compartilhar via


Armadilhas em potencial com PLINQ

Em muitos casos, o PLINQ pode fornecer melhorias de desempenho significativas em relação a consultas sequenciais do LINQ to Objects. No entanto, o trabalho de paralelizar a execução da consulta apresenta 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 evitar ao escrever consultas PLINQ.

Não suponha que o paralelo é sempre mais rápido

A paralelização, por vezes, faz com que uma consulta PLINQ seja executada de forma mais devagar do que seu LINQ to Object equivalente. A regra básica é que as consultas que têm poucos elementos fonte e delegados de usuários rápidos provavelmente não acelerarão muito. No entanto, como muitos fatores estão envolvidos no desempenho, recomendamos avaliar os resultados reais antes de decidir pelo uso do PLINQ. Para saber mais, veja Noções básicas sobre agilização em PLINQ.

Evite gravar em locais de memória compartilhada

No 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 acessando essas variáveis simultaneamente, existe 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 o excesso de paralelização

Ao usar o método AsParallel, você incorre em custos indiretos de particionar a coleção de origem e sincronizar os 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 executando vários threads vinculados à computação em apenas um processador. Portanto, você deve ter cuidado para não paralelizar excessivamente uma consulta.

O cenário mais comum em que a paralelização excessiva pode ocorrer é em consultas aninhadas, como mostrado no seguinte snippet.

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 interna de dados (cust.Orders) é muito longa.

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

  • O sistema de destino tem processadores suficientes para lidar com o número de threads que serão produzidos paralelizando a consulta em cust.Orders.

Em todos os casos, a melhor maneira de determinar a forma ideal da consulta é testar e medir. Para saber mais, confira Como avaliar o desempenho de consulta PLINQ.

Evite chamadas para métodos não thread-safe

Gravar para métodos de instância não thread-safe a partir de uma consulta PLINQ pode levar à corrupção de dados que pode ou não ser detectada no seu programa. Isso também poderá levar a exceções. No exemplo a seguir, vários segmentos estão tentando chamar simultaneamente o método FileStream.Write, que não é compatível com a 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));

Limite chamadas para métodos thread-safe

A maioria dos métodos estáticos no .NET é thread-safe e pode ser chamada de vários threads simultaneamente. No entanto, mesmo nesses casos, a sincronização envolvida pode levar a uma desaceleração significativa na consulta.

Observação

Você pode testar isso sozinho inserindo algumas chamadas para WriteLine nas suas consultas. Embora esse 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 classificação desnecessárias

Quando o PLINQ executa uma consulta em paralelo, divide a sequência de origem em partições que podem ser operadas simultaneamente em múltiplos segmentos. Por padrão, a ordem em que as partições são processadas e os resultados fornecidos não é previsível (exceto para operadores como OrderBy). Você pode instruir o PLINQ a preservar a classificação de qualquer sequência de origem, mas isso tem um impacto negativo no desempenho. A prática recomendada, sempre que possível, é estruturar consultas para que elas não dependam da preservação da classificação. Para saber mais, veja Preservação da ordem em PLINQ.

Dê preferência a ForAll em vez de ForEach quando possível

Embora o PLINQ execute uma consulta em múltiplos threads, se você consumir os resultados em um loop foreach (For Each no Visual Basic), os resultados da consulta devem ser mesclados novamente em um thread e acessados em série pelo enumerador. Em alguns casos, isso é inevitável. No entanto, sempre que possível, use o método ForAll para habilitar cada thread a gerar seus próprios resultados, por exemplo, gravando para uma coleção thread-safe como System.Collections.Concurrent.ConcurrentBag<T>.

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

Esteja ciente de questões de afinidade de thread

Algumas tecnologias, por exemplo, interoperabilidade COM para componentes de um único segmento (STA), 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, tanto no Windows Forms quanto no WPF, um controle só pode ser acessado no thread em que foi criado. Se você tenta acessar o estado compartilhado de um controle Windows Forms em uma consulta PLINQ, uma exceção é gerada se você estiver executando no depurador. (Essa configuração pode ser desativada.) No entanto, se sua consulta for consumada no thread da IU, você pode acessar o controle do loop foreach que enumera os resultados da consulta, pois esse código é executado em apenas um thread.

Não suponha que iterações de ForEach, For e ForAll sempre são executadas em paralelo

É importante ter em mente que iterações individuais em um loop Parallel.For, Parallel.ForEach ou ForAll podem, mas não necessariamente precisam, ser executadas em paralelo. Portanto, você deve evitar gravar qualquer código que dependa da correção na execução paralela de iterações ou na execução de iterações em qualquer classificação específica.

Por exemplo, esse código é provavelmente um deadlock:

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 do evento tenha sido concluída. No entanto, é possível que as iterações em espera bloqueiem todos os threads que são usados para executar o loop paralelo, antes que a iteração de configuração do evento tenha tido a chance de ser executada. Isso resulta em um deadlock – a iteração de configuração do evento nunca será executada e as iterações em espera nunca serão ativadas.

Em particular, uma iteração de um loop paralelo nunca deve aguardar outra iteração do loop para progredir. Se o loop paralelo decidir agendar as iterações sequencialmente, mas em ordem oposta, ocorrerá um deadlock.

Confira também