Possíveis armadilhas em dados e o paralelismo de tarefas

Em muitos casos, Parallel.For e Parallel.ForEach pode fornecer melhorias de desempenho significativos sobre loops seqüencial comum. No entanto, o trabalho de paralelização do loop 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ê escreve loops paralelos.

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

Em alguns casos um loop em paralelo pode ser executado mais lentamente do que o seu equivalente seqüencial. O princípio básico é que os loops paralelos que possuem algumas iterações e 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 você sempre medir os resultados reais.

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 um loop paralelo tanto quanto possível compartilhado. A melhor maneira de fazer isso é usar os métodos sobrecarregados de Parallel.For e Parallel.ForEach que usam um System.Threading.ThreadLocal<T> variável para armazenar o estado de segmento local durante a execução do loop. Para obter mais informações, consulte Como: Criar um Loop de Parallel tem variáveis de segmento locais e Como: Criar um Loop de Parallel tem variáveis de segmento locais.

Evite Over-Parallelization

Usando loops paralelos, incorrer em custos de sobrecarga 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 um loop.

O cenário mais comum na qual over-parallelization pode ocorrer é em loops aninhados. Na maioria dos casos, é melhor paralelizar somente o loop externo, a menos que uma ou mais das seguintes condições se aplicam:

  • O loop interno é 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.

Evitar chamadas para métodos de Thread Safe

Gravar os métodos de instância de thread safe loop paralelo 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.WriteByte método simultaneamente, que não há suporte para a classe.

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)))
FileStream fs = File.OpenWrite(path);
byte[] bytes = new Byte[10000000];
// ...
Parallel.For(0, bytes.Length, (i) => fs.WriteByte(bytes[i]));

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

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

Tenha cuidado quando aguardando delegados são chamados de Parallel

Em determinadas circunstâncias, a biblioteca paralela de tarefas será embutido uma tarefa, o que significa que ele é executado na tarefa no thread de execução no momento. (Para obter mais informações, consulte Agendadores de tarefa.) A otimização de desempenho pode levar a deadlock em determinados casos. Por exemplo, duas tarefas podem executar o mesmo delegado código, o qual sinaliza quando ocorre um evento, e em seguida, aguarda sinalizar a outra tarefa. Se a segunda tarefa é embutida no mesmo thread como o primeiro e o primeiro entra em estado de espera, a segunda tarefa nunca poderá sinalizar o evento. Para evitar uma ocorrência desse tipo, você pode especificar um tempo limite na operação de espera ou construtores de thread explícita de uso para ajudar a garantir que uma tarefa não podem bloquear o outro.

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

Evitar a execução de Loops paralelos no Thread da interface do usuário

É importante manter a interface de usuário (UI) do seu aplicativo responsivo. Se uma operação contém suficiente trabalho para garantir a paralelização, em seguida, ele provavelmente não deve ser executado no thread da interface do usuário que a operação. Em vez disso, ele deve descarregar a operação a ser executado em um segmento de plano de fundo. Por exemplo, se você quiser usar um loop paralelo para computar alguns dados que, em seguida, devem ser processados em um controle de interface do usuário, você deve considerar executando o loop dentro de uma instância de tarefa, em vez de fazê-lo diretamente em um manipulador de eventos da interface do usuário. Somente quando o cálculo do núcleo foi concluída deve você então empacotar a atualização de interface do usuário do thread de interface do usuário.

Se você executar loops paralelos no thread da interface do usuário, tenha cuidado com a atualização de controles UI de dentro do loop. Tentativa de atualizar a interface do usuário controles a partir de um loop paralelo que está sendo executado no thread da interface do usuário podem levar à corrupção de estado, exceções, atualizações atrasadas 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 segmento 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 está sendo executado em um segmento de plano de fundo (como For pode fazer), a chamada de Invoke faz com que uma mensagem para ser enviado para o segmento UI e blocos aguardando essa mensagem ser processado. Desde que o segmento de interface do usuário é bloqueado executando o For, a mensagem nunca pode ser processada e travamentos de segmento UI.

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
private void button1_Click(object sender, EventArgs e)
{
    Parallel.For(0, N, i =>
    {
        // do work for i
        button1.Invoke((Action)delegate { DisplayProgress(i); });
    });
}

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

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
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); });
        })
         );
}

Consulte também

Conceitos

Programação em paralela a.NET Framework

Possíveis armadilhas com PLINQ

Outros recursos

Padrões de programação paralela: Compreendendo e aplicando paralela padrões com o.NET Framework 4