Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O multithreading requer uma programação cuidadosa. Para a maioria das tarefas, você pode reduzir a complexidade ao enfileirar solicitações para a execução por parte de threads de pool. Este tópico aborda situações mais difíceis, como coordenar o trabalho de vários threads ou lidar com 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.
Deadlocks e condições de corrida
O multithreading resolve problemas com taxa de transferência e capacidade de resposta, mas, ao fazer isso, ele introduz novos problemas: deadlocks e condições de corrida.
Deadlocks
Um deadlock ocorre quando cada um dos dois threads tenta bloquear um recurso que o outro já bloqueou. Nenhum thread pode fazer mais progresso.
Muitos métodos das classes de threading gerenciadas fornecem tempos limite para ajudá-lo a detectar 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 retornará 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 de corrida
Uma condição de corrida é um bug que ocorre quando o resultado de um programa depende de qual dos dois ou mais threads alcança um determinado bloco de código primeiro. A execução do programa muitas vezes produz resultados diferentes e o resultado de uma determinada 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). Essa operação requer carregar o valor de objCt em um registro, incrementar o valor e armazená-lo em objCt.
Em um aplicativo com multithreading, um thread que carregou e incrementou o valor pode ser impedido por outro thread que execute todas as três etapas; quando o primeiro thread retomar a execução e armazenar seu valor, ele substituirá objCt sem levar em conta o fato de que o valor foi alterado durante o processo.
Essa condição de corrida específica é facilmente evitada usando métodos da Interlocked classe, como Interlocked.Increment. Para ler sobre outras técnicas para sincronizar dados entre vários threads, consulte Sincronizando dados para multithreading.
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, é preciso considerar o que poderia acontecer se um thread fosse impedido antes de executar a linha (ou antes de qualquer uma das instruções de máquina individuais que compõem a linha) e outro thread o substituísse.
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 de ser executado. Para impedir a execução de código em um tipo 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 da classe tenha concluído sua execução.
Por exemplo, se um construtor de classe iniciar um novo thread, e o procedimento do thread chamar um membro static da classe, o novo thread bloqueia até que o construtor da classe seja concluído.
Isso se aplica a qualquer tipo que possa ter um construtor static.
Número de processadores
Quer haja vários processadores ou apenas um processador disponível em um sistema, isso 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 no runtime.
Recomendações gerais
Considere as seguintes diretrizes ao usar vários threads:
Não use Thread.Abort para encerrar outros threads. Chamar
Abortem outro thread é semelhante a lançar uma exceção nesse thread, sem saber que ponto esse thread atingiu em seu processamento.Não use Thread.Suspend e Thread.Resume para sincronizar as atividades de múltiplos threads. Use Mutex, ManualResetEvent, AutoResetEvent, e Monitor.
Não controle a execução de threads de trabalho do seu programa principal (usando eventos, por exemplo). Em vez disso, projete seu programa de forma que os threads de trabalho sejam responsáveis por esperar até que o trabalho esteja disponível, executá-lo e notificar outras partes do seu programa quando terminar. Se seus threads de trabalho não bloquearem, considere o uso de threads de pool. 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ódigo como
lock(typeof(X))em C# ouSyncLock(GetType(X))no Visual Basic, ou o uso de Monitor.Enter com objetos Type. Para um determinado tipo, há apenas uma instância de System.Type por domínio de aplicativo. Se o tipo no qual você usar um bloqueio for público, o código que não for o seu próprio poderá assumir bloqueios, levando a deadlocks. Para questões adicionais, consulte Práticas recomendadas para confiabilidade.Tenha cuidado ao bloquear instâncias, por exemplo
lock(this), em C# ouSyncLock(Me)no Visual Basic. Se outro código no seu aplicativo, externo ao tipo, assumir um bloqueio no objeto, podem ocorrer deadlocks.Garanta que uma thread que entrou em um monitor sempre saia desse monitor, mesmo que uma exceção ocorra enquanto a thread estiver no monitor. A instrução lock C# e a instrução SyncLock do Visual Basic fornecem esse comportamento automaticamente, utilizando um bloco finally para garantir que ele 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 o possui atualmente é 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 em ter seu próprio thread, porque esse thread bloqueará durante as operações de E/S e, assim, 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 a computação intensiva coexiste com a entrada do usuário e com tarefas que envolvem E/S, mas várias tarefas com uso intensivo de computação lidam entre si.
Considere usar métodos da Interlocked classe para alterações de estado simples, em vez de usar a
lockinstrução (SyncLockno Visual Basic). A instruçãolocké uma boa ferramenta para fins gerais, mas a classe Interlocked oferece um melhor desempenho para atualizações que devem ser atômicas. Internamente, ela executa um único prefixo de bloqueio se não houver nenhuma contenção. Em revisões de código, observe o código como o mostrado nos exemplos a seguir. No primeiro exemplo, uma variável de estado é incrementada:SyncLock lockObject myField += 1 End SyncLocklock(lockObject) { myField++; }Você pode melhorar o desempenho usando o Increment método em vez da
lockinstrução, da seguinte maneira:System.Threading.Interlocked.Increment(myField)System.Threading.Interlocked.Increment(myField);Observação
Use o Add método para incrementos atômicos maiores que 1.
No segundo exemplo, uma variável de tipo de referência será atualizada somente se for uma referência nula (
Nothingno Visual Basic).If x Is Nothing Then SyncLock lockObject If x Is Nothing Then x = y End If End SyncLock End Ifif (x == null) { lock (lockObject) { x ??= y; } }O desempenho pode ser melhorado usando o CompareExchange método, da seguinte maneira:
System.Threading.Interlocked.CompareExchange(x, y, Nothing)System.Threading.Interlocked.CompareExchange(ref x, y, null);Observação
A sobrecarga do método CompareExchange<T>(T, T, T) fornece uma alternativa fortemente tipada para tipos de referência.
Recomendações para bibliotecas de classes
Considere as seguintes diretrizes ao criar bibliotecas de classes para multithreading:
Evite a necessidade de sincronização, se possível. Isso é especialmente verdadeiro para código fortemente usado. Por exemplo, um algoritmo pode ser ajustado para tolerar uma condição de competição entre processos em vez de eliminá-la. A sincronização desnecessária diminui o desempenho e cria a possibilidade de deadlocks e condições de competição.
Torne o thread de dados estáticos (
Sharedno Visual Basic) seguro por padrão.Não torne dados de instância thread-safe por padrão. Adicionar bloqueios para criar códigos de thread-safe reduz o desempenho, aumenta a contenção de bloqueios e cria a possibilidade de deadlocks. Em modelos comuns de aplicativos, apenas um thread por vez executa o código do usuário, o que minimiza a necessidade de segurança de thread. Por esse motivo, as bibliotecas de classes do .NET não são thread-safe por padrão.
Evite fornecer métodos estáticos que alterem o estado estático. Em cenários comuns de servidor, 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 bugs de threading. Considere usar 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 poderão resultar em deadlocks ou sincronização redundante, afetando negativamente o desempenho.