Armadilhas potenciais no paralelismo de dados e tarefas

Em muitos casos, Parallel.For e Parallel.ForEach pode fornecer melhorias significativas de desempenho em relação a loops sequenciais comuns. No entanto, o trabalho de paralelização do loop 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 quando você escreve loops paralelos.

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

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

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

Evite a paralelização excessiva

Usando loops paralelos, você incorre nos custos gerais de particionar a coleção de origem e sincronizar os threads de trabalho. 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, você deve ter cuidado para não paralelizar demais 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 um cálculo caro 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.

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

Gravar em métodos de instância não thread-safe a partir de um loop paralelo 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ários threads estariam tentando chamar o método simultaneamente, o FileStream.WriteByte que não é suportado pela 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)))

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.

Nota

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

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

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

Em determinadas circunstâncias, a Biblioteca Paralela de Tarefas embutirá uma tarefa, o que significa que ela é executada na tarefa no thread em execução no momento. (Para obter mais informações, consulte Agendadores de tarefas.) Essa otimização de desempenho pode levar a um impasse em certos casos. Por exemplo, duas tarefas podem executar o mesmo código delegado, que sinaliza quando ocorre um evento e, em seguida, aguarda o sinal da outra tarefa. Se a segunda tarefa estiver embutida no mesmo thread que a primeira e a primeira entrar em um estado de espera, a segunda tarefa nunca poderá sinalizar seu evento. Para evitar tal ocorrência, você pode especificar um tempo limite na operação Wait ou usar construtores de thread explícitos para ajudar a garantir que uma tarefa não possa bloquear a outra.

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

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

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 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 em outra iteração do loop para progredir. Se o loop paralelo decidir agendar as iterações sequencialmente, mas na ordem oposta, ocorrerá um impasse.

Evite executar loops paralelos no thread da interface do usuário

É importante manter a interface do usuário (UI) do seu aplicativo responsiva. Se uma operação contiver trabalho suficiente para garantir paralelização, ela provavelmente não deve ser executada no thread da interface do usuário. Em vez disso, ele deve descarregar essa operação para ser executada em um thread em segundo plano. Por exemplo, se você quiser usar um loop paralelo para calcular alguns dados que devem ser renderizados em um controle de interface do usuário, considere executar o loop em uma instância de tarefa em vez de diretamente em um manipulador de eventos de interface do usuário. Somente quando a computação principal estiver concluída, você deverá organizar a atualização da interface do usuário de volta para o thread da 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 da interface do usuário de dentro do loop. A tentativa de atualizar os controles da interface do usuário de dentro de um loop paralelo que está sendo executado no thread da interface do usuário pode levar a corrupção de estado, exceções, atualizações atrasadas e até mesmo bloqueios, dependendo de como a atualização da interface do usuário é invocada. No exemplo a seguir, o loop paralelo bloqueia o thread da interface do usuário no qual está sendo executado até que todas as iterações sejam concluídas. No entanto, se uma iteração do loop estiver sendo executada em um thread em segundo plano (como For pode fazer), a chamada para Invoke fará com que uma mensagem seja enviada para o thread da interface do usuário e bloqueará a espera que essa mensagem seja processada. Como o thread da interface do usuário está bloqueado executando o , a mensagem nunca pode ser processada Fore o thread da interface do usuário fica bloqueado.

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 da 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

Consulte também