Práticas recomendadas de threading gerenciado

Multithreading requer uma programação cuidadosa. Para a maioria das tarefas, você pode reduzir a complexidade enfileirando solicitações para execução por threads do pool de threads. Este tópico aborda situações mais difíceis, como coordenar o trabalho de vários threads ou manipular threads que bloqueiam.

Nota

A partir do .NET Framework 4, a Biblioteca Paralela de Tarefas e o PLINQ fornecem APIs que reduzem parte da complexidade e dos riscos da programação multi-threaded. Para obter mais informações, consulte Programação paralela no .NET.

Impasses e condições de corrida

Multithreading resolve problemas com taxa de transferência e capacidade de resposta, mas ao fazê-lo introduz novos problemas: impasses e condições de corrida.

Impasses

Um deadlock ocorre quando cada um dos dois threads tenta bloquear um recurso que o outro já bloqueou. Nenhum dos dois segmentos pode fazer mais progressos.

Muitos métodos das classes de threading gerenciadas fornecem tempos limite para ajudá-lo a detetar deadlocks. Por exemplo, o código a seguir tenta adquirir um bloqueio em um objeto chamado lockObject. Se o bloqueio não for obtido em 300 milissegundos, Monitor.TryEnter retorna false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

Condições da corrida

Uma condição de corrida é um bug que ocorre quando o resultado de um programa depende de qual de dois ou mais threads atinge um determinado bloco de código primeiro. Executar o programa muitas vezes produz resultados diferentes, e o resultado de qualquer execução não pode ser previsto.

Um exemplo simples de uma condição de corrida é incrementar um campo. Suponha que uma classe tenha um campo estático privado (Compartilhado no Visual Basic) que é incrementado sempre que uma instância da classe é criada, usando código como objCt++; (C#) ou objCt += 1 (Visual Basic). Esta operação requer carregar o valor de objCt um registro, incrementar o valor e armazená-lo no objCt.

Em um aplicativo multithreaded, um thread que carregou e incrementou o valor pode ser antecipado por outro thread que executa todas as três etapas; Quando o primeiro thread retoma a execução e armazena seu valor, ele substitui objCt sem levar em conta o fato de que o valor foi alterado nesse ínterim.

Esta condição de corrida em particular é facilmente evitada usando métodos da Interlocked classe, como Interlocked.Increment. Para ler sobre outras técnicas de sincronização de dados entre vários threads, consulte Sincronizando dados para multithreading.

As condições de corrida também podem ocorrer quando você sincroniza as atividades de vários threads. Sempre que você escreve uma linha de código, você deve considerar o que pode acontecer se um thread fosse antecipado antes de executar a linha (ou antes de qualquer uma das instruções individuais da máquina que compõem a linha) e outro thread a ultrapassasse.

Membros estáticos e construtores estáticos

Uma classe não é inicializada até que seu construtor de classe (static construtor em C#, Shared Sub New no Visual Basic) tenha terminado a execução. Para impedir a execução de código em um tipo que não é inicializado, o common language runtime bloqueia todas as chamadas de outros threads para static membros da classe (Shared membros no Visual Basic) até que o construtor de classe tenha terminado a execução.

Por exemplo, se um construtor de classe inicia um novo thread e o procedimento de thread chama um static membro da classe, o novo thread bloqueia até que o construtor de classe seja concluído.

Isso se aplica a qualquer tipo que possa ter um static construtor.

Número de processadores

Se há vários processadores ou apenas um processador disponível em um sistema pode influenciar a arquitetura multithreaded. Para obter mais informações, consulte Número de processadores.

Use a Environment.ProcessorCount propriedade para determinar o número de processadores disponíveis em tempo de execução.

Recomendações gerais

Considere as seguintes diretrizes ao usar vários threads:

  • Não use Thread.Abort para encerrar outros threads. Chamar Abort outro thread é semelhante a lançar uma exceção nesse thread, sem saber a que ponto esse thread chegou em seu processamento.

  • Não use Thread.Suspend e Thread.Resume sincronize as atividades de vários threads. Utilize Mutex, ManualResetEvent, AutoResetEvent, e Monitor.

  • Não controle a execução de threads de trabalho a partir do seu programa principal (usando eventos, por exemplo). Em vez disso, projete seu programa para que os threads de trabalho sejam responsáveis por aguardar até que o trabalho esteja disponível, executá-lo e notificar outras partes do programa quando terminar. Se os threads de trabalho não bloquearem, considere o uso de threads de pool de threads. Monitor.PulseAll é útil em situações em que os threads de trabalho bloqueiam.

  • Não use tipos como objetos de bloqueio. Ou seja, evite códigos como lock(typeof(X)) em C# ou SyncLock(GetType(X)) no Visual Basic, ou o uso de Monitor.Enter com Type objetos. Para um determinado tipo, há apenas uma instância de por domínio de System.Type aplicativo. Se o tipo em que você usa um cadeado é público, um código diferente do seu pode ter bloqueios nele, levando a impasses. Para problemas adicionais, consulte Práticas recomendadas de confiabilidade.

  • Tenha cuidado ao bloquear instâncias, por exemplo lock(this) , em C# ou SyncLock(Me) no Visual Basic. Se outro código em seu aplicativo, externo ao tipo, tiver um bloqueio no objeto, poderão ocorrer deadlocks.

  • Certifique-se de que um thread que entrou em um monitor sempre sai desse monitor, mesmo que ocorra uma exceção enquanto o thread estiver no monitor. A instrução de bloqueio C# e a instrução SyncLock do Visual Basic fornecem esse comportamento automaticamente, empregando um bloco final para garantir que Monitor.Exit seja chamado. Se você não puder garantir que Exit será chamado, considere alterar seu design para usar Mutex. Um mutex é liberado automaticamente quando o thread que atualmente o possui é encerrado.

  • Use vários threads para tarefas que exigem recursos diferentes e evite atribuir vários threads a um único recurso. Por exemplo, qualquer tarefa que envolva E/S se beneficia de ter seu próprio thread, porque esse thread será bloqueado durante as operações de E/S e, portanto, permitirá que outros threads sejam executados. A entrada do usuário é outro recurso que se beneficia de um thread dedicado. Em um computador de processador único, uma tarefa que envolve computação intensiva coexiste com a entrada do usuário e com tarefas que envolvem E/S, mas várias tarefas de computação intensiva competem entre si.

  • Considere o Interlocked uso de métodos da classe para alterações de estado simples, em vez de usar a lock instrução (SyncLock no Visual Basic). A lock instrução é uma boa ferramenta de uso geral, mas a Interlocked classe fornece melhor desempenho para atualizações que devem ser atômicas. Internamente, ele executa um único prefixo de bloqueio se não houver contenção. Em revisões de código, observe códigos como os mostrados nos exemplos a seguir. No primeiro exemplo, uma variável de estado é incrementada:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)
    {  
        myField++;  
    }  
    

    Você pode melhorar o desempenho usando o Increment método em vez da lock instrução, da seguinte maneira:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Nota

    Utilize o método para incrementos atómicos superiores a Add 1.

    No segundo exemplo, uma variável de tipo de referência é atualizada somente se for uma referência nula (Nothing no Visual Basic).

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    O desempenho pode ser melhorado usando o CompareExchange método em vez disso, da seguinte maneira:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Nota

    A CompareExchange<T>(T, T, T) sobrecarga de método fornece uma alternativa segura para tipos de referência.

Recomendações para bibliotecas de classes

Considere as seguintes diretrizes ao projetar bibliotecas de classes para multithreading:

  • Evite a necessidade de sincronização, se possível. Isso é especialmente verdadeiro para código muito usado. Por exemplo, um algoritmo pode ser ajustado para tolerar uma condição de raça em vez de eliminá-la. A sincronização desnecessária diminui o desempenho e cria a possibilidade de impasses e condições de corrida.

  • Torne o thread de dados estáticos (Shared no Visual Basic) seguro por padrão.

  • Não torne o thread de dados da instância seguro por padrão. Adicionar bloqueios para criar código thread-safe diminui o desempenho, aumenta a contenção de bloqueios e cria a possibilidade de ocorrência de deadlocks. Em modelos de aplicativos comuns, apenas um thread de cada vez executa o código do usuário, o que minimiza a necessidade de segurança do thread. Por esse motivo, as bibliotecas de classe .NET não são thread safe por padrão.

  • Evite fornecer métodos estáticos que alteram o estado estático. Em cenários de servidor comuns, o estado estático é compartilhado entre solicitações, o que significa que vários threads podem executar esse código ao mesmo tempo. Isso abre a possibilidade de threading bugs. Considere o uso de um padrão de design que encapsula dados em instâncias que não são compartilhadas entre solicitações. Além disso, se os dados estáticos forem sincronizados, as chamadas entre métodos estáticos que alteram o estado podem resultar em deadlocks ou sincronização redundante, afetando negativamente o desempenho.

Consulte também