Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
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
Abortem 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# ouSyncLock(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# ouSyncLock(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(SyncLockno Visual Basic). Alockdeclaraçã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 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 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 (
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 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 (
Sharedno 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.