Armadilhas em potencial em dados e paralelismo da tarefa

Em muitos casos, o Parallel.For e Parallel.ForEach podem melhorar consideravelmente o desempenho em comparação com consultas sequenciais comuns. No entanto, o trabalho de paralelizar o loop 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 serem evitadas ao escrever loops paralelos.

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

Em certos casos, um loop paralelo pode executar com mais lentidão do que seu equivalente sequencial. A regra básica é que os loops paralelos que têm poucas interações e delegados de usuários rápidos provavelmente não acelerarão muito. No entanto, como muitos fatores estão envolvidos no desempenho, recomendamos sempre a medição dos resultados reais.

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 um loop paralelo tanto quanto possível. A melhor maneira de fazer isso é usar as sobrecargas de Parallel.For e Parallel.ForEach que usam uma variável System.Threading.ThreadLocal<T> para armazenar o estado de local de thread durante a execução do loop. Para obter mais informações, consulte Como gravar um loop Parallel.For com variáveis locais de thread e Como gravar um loop Parallel.ForEach com variáveis locais de partição.

Evite o excesso de paralelização

Ao usar loops paralelos, 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 um loop.

O cenário mais comum em que a paralelização excessiva pode ocorrer é em loops aninhados. Na maioria dos casos, é melhor paralelizar apenas o loop externo, a menos que uma ou mais das seguintes condições se apliquem:

  • O loop interno é conhecido por ser muito longo.

  • 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.

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

Gravar para métodos de instância não thread-safe a partir de um loop paralelo pode levar à corrupção de dados, o que pode ou não ser detectada em 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.WriteByte, que não é compatível com a classe.

FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));
Dim fs As FileStream = File.OpenWrite(filepath)
Dim bytes() As Byte
ReDim bytes(1000000)
' ...init byte array
Parallel.For(0, bytes.Length, Sub(n) fs.WriteByte(bytes(n)))

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 loops paralelos, a menos que seja necessário.

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. Por exemplo, isso significa que você não pode atualizar um controle de lista de um loop paralelo, a menos que você configure o agendador de thread para agendar o trabalho somente no thread da interface do usuário. Para obter mais informações, confira Especificação de um contexto de sincronização.

Tenha cuidado ao aguardar delegados que são chamados por Parallel.Invoke

Em determinadas circunstâncias, a biblioteca de paralelismo de tarefas embutirá uma tarefa, ou seja, ela será executada na tarefa no thread em execução no momento. (Para mais informações, confira Agendadores de Tarefas.) Essa otimização de desempenho pode levar ao deadlock em determinados casos. Por exemplo, duas tarefas podem executar o mesmo código de delegado, o qual sinaliza quando ocorre um evento e, em seguida, aguarda a sinalização da outra tarefa. Se a segunda tarefa for embutida no mesmo thread que a primeira, e a primeira entrar em um estado de Espera, a segunda tarefa nunca poderá sinalizar o evento. Para evitar isso, especifique um tempo limite para a operação de Espera, ou use constructos de thread explícitos para ajudar a garantir que uma tarefa não possa bloquear a outra.

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

É importante ter em mente que iterações individuais em um loop For, ForEach ou ForAll podem, mas não necessariamente têm que, 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:

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
Dim mres = 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)
                mres.Set()
            Else
                Console.WriteLine("Waiting on {0} with value of {1}",
                                  Thread.CurrentThread.ManagedThreadId, j)
                mres.Wait()
            End If
        End Sub) ' 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.

Evitar loops paralelos em execução no thread de interface do usuário

É importante manter a interface do usuário (IU) do aplicativo responsiva. Se uma operação contiver trabalho suficiente para garantir a paralelização, provavelmente ela não deverá ser executada no thread da interface do usuário. Em vez disso, deverá ser descarregada para execução em um thread em segundo plano. Por exemplo, se você quiser usar um loop paralelo para calcular alguns dados que, depois, deverão ser renderizados em um controle de interface do usuário, convém executar o loop dentro de uma instância de tarefa, em vez de diretamente em um manipulador de eventos de interface do usuário. Somente após a conclusão do cálculo principal você deverá realizar marshaling da atualização da interface do usuário para o thread de interface do usuário.

Se você executar loops paralelos no thread da interface do usuário, tenha cuidado para evitar a atualização dos controles de interface do usuário de dentro do loop. A tentativa de atualizar os controles de interface do usuário de dentro de um loop paralelo em execução no thread da interface do usuário poderá causar a corrupção do estado, exceções, atraso nas atualizações e até mesmo deadlocks, dependendo de como a atualização da interface do usuário é invocada. No exemplo a seguir, o loop paralelo bloqueia o thread de interface do usuário no qual ele está em execução até que todas as iterações sejam concluídas. No entanto, se uma iteração do loop estiver em execução em um thread em segundo plano (como For pode fazer), a chamada a Invoke faz com que uma mensagem seja enviada ao thread de interface do usuário e causa um bloqueio aguardando o processo da mensagem. Como o thread da interface do usuário está bloqueado executando For, a mensagem nunca pode ser processada e o thread da interface do usuário sofre um deadlock.

private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Parallel.For(0, iterations, Sub(x)
                                    Button1.Invoke(Sub()
                                                       DisplayProgress(x)
                                                   End Sub)
                                End Sub)
End Sub

O exemplo a seguir mostra como evitar o deadlock, executando o loop dentro de uma instância de tarefa. O thread de interface do usuário não é bloqueado pelo loop, e a mensagem pode ser processada.

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
        Parallel.For(0, N, i =>
        {
            // do work for i
            button1.Invoke((Action)delegate { DisplayProgress(i); });
        })
         );
}
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

    Dim iterations As Integer = 20
    Task.Factory.StartNew(Sub() Parallel.For(0, iterations, Sub(x)
                                                                Button1.Invoke(Sub()
                                                                                   DisplayProgress(x)
                                                                               End Sub)
                                                            End Sub))
End Sub

Confira também