Partilhar via


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.

Observação

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 concorrência

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.

Bloqueios

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 concorrência é um erro que ocorre quando o resultado de um programa depende de qual dos dois ou mais fluxos de execução atinja primeiro um determinado bloco de código. 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 o/a programador/a escreve uma linha de código, deve considerar o que pode acontecer se um fio de execução for preemptado antes de executar a linha (ou antes de qualquer uma das instruções individuais da máquina que compõem a linha) e outro fio de execução o ultrapassar.

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 aplica-se a qualquer tipo que possa ter um construtor static.

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 em outra thread é semelhante a lançar uma exceção nessa thread sem saber a que ponto ela chegou no seu processamento.

  • Não use Thread.Suspend e Thread.Resume para sincronizar as atividades de várias tarefas. 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 ficam bloqueados.

  • 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 System.Type por domínio de aplicação. 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 lock C# e a instrução SyncLock do Visual Basic fornecem esse comportamento automaticamente, empregando um bloco finally 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 o seu próprio fio de execução, porque esse fio de execução será bloqueado durante as operações de E/S e, portanto, permitirá que outros fios de execução 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 uso de métodos da classe Interlocked para alterações de estado simples, em vez de usar a instrução lock (SyncLock no Visual Basic). A lock declaração é uma boa ferramenta de uso geral, mas a Interlocked classe oferece um desempenho superior para atualizações que devem ser atomizadas. 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);
    

    Observação

    Use o método Add para incrementos atómicos superiores a 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);
    

    Observação

    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 erros de encadeamento. 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.

Ver também