Condições de corrida e impasses

O Visual Basic .NET ou Visual Basic oferece a capacidade de usar threads em aplicativos do Visual Basic pela primeira vez. Os threads introduzem problemas de depuração, como condições de corrida e impasses. Este artigo explora esses dois problemas.

Versão original do produto: Visual Basic, Visual Basic .NET
Número de KB original: 317723

Quando ocorrem condições de corrida

Uma condição de corrida ocorre quando dois threads acessam uma variável compartilhada ao mesmo tempo. O primeiro thread lê a variável e o segundo thread lê o mesmo valor da variável. Em seguida, o primeiro thread e o segundo thread executam suas operações no valor e correm para ver qual thread pode gravar o valor por último na variável compartilhada. O valor do thread que grava seu valor por último é preservado, pois o thread está escrevendo sobre o valor que o thread anterior escreveu.

Detalhes e exemplos para uma condição de corrida

Cada thread é alocado um período predefinido de tempo para ser executado em um processador. Quando a hora alocada para o thread expirar, o contexto do thread é salvo até a próxima ativação do processador e o processador inicia a execução do próximo thread.

Como um comando de uma linha pode causar uma condição de corrida

Examine o exemplo a seguir para ver como ocorre uma condição de corrida. Há dois threads e ambos estão atualizando uma variável compartilhada chamada total (que é representada como dword ptr ds:[031B49DCh] no código do assembly).

  • Thread 1

    Total = Total + val1
    
  • Thread 2

    Total = Total - val2
    

Código do assembly (com números de linha) da compilação do código do Visual Basic anterior:

  • Thread 1

    1. mov eax,dword ptr ds:[031B49DCh]
    2. add eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 7611097F
    6. mov dword ptr ds:[031B49DCh],eax
    
  • Thread 2

    1. mov eax,dword ptr ds:[031B49DCh]
    2. sub eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 76110BE7
    6. mov dword ptr ds:[031B49DCh],eax
    

Ao examinar o código do assembly, você pode ver quantas operações o processador está executando no nível inferior para executar um cálculo de adição simples. Um thread pode ser capaz de executar todo ou parte de seu código de assembly durante seu tempo no processador. Agora veja como uma condição de corrida ocorre a partir deste código.

Total tem 100, val1 é 50 e val2 15. O Thread 1 tem a oportunidade de ser executado, mas só conclui as etapas 1 a 3. Isso significa que o Thread 1 leu a variável e concluiu a adição. O Thread 1 agora está apenas esperando para gravar seu novo valor de 150. Depois que o Thread 1 for interrompido, o Thread 2 será executado completamente. Isso significa que ele escreveu o valor calculado (85) para a variável Total. Por fim, o Thread 1 recupera o controle e conclui a execução. Ele grava seu valor (150). Portanto, quando o Thread 1 for concluído, o valor de Total agora é 150 em vez de 85.

Você pode ver como isso pode ser um grande problema. Se este for um programa bancário, o cliente teria dinheiro em sua conta que não deveria estar presente.

Esse erro é aleatório, pois é possível que o Thread 1 conclua sua execução antes que a hora do processador expire e, em seguida, o Thread 2 pode iniciar sua execução. Se esses eventos ocorrerem, o problema não ocorrerá. A execução do thread não édeterminística, portanto, você não pode controlar a hora ou a ordem de execução. Observe também que os threads podem ser executados de forma diferente no modo runtime versus depuração. Além disso, você pode ver que, se você executar cada thread na série, o erro não ocorrerá. Essa aleatoriedade torna esses erros muito mais difíceis de rastrear e depurar.

Para evitar que as condições de corrida ocorram, você pode bloquear variáveis compartilhadas, de modo que apenas um thread por vez tenha acesso à variável compartilhada. Faça isso com moderação, pois se uma variável estiver bloqueada no Thread 1 e no Thread 2 também precisar da variável, a execução do Thread 2 será interrompida enquanto o Thread 2 aguarda o Thread 1 para liberar a variável. (Para obter mais informações, confira SyncLock na seção Referências deste artigo.)

Sintomas de uma condição racial

O sintoma mais comum de uma condição de raça é valores imprevisíveis de variáveis compartilhadas entre vários threads. Isso resulta da imprevisibilidade da ordem na qual os threads são executados. Às vezes, um thread ganha e, em algum momento, o outro thread ganha. Em outros momentos, a execução funciona corretamente. Além disso, se cada thread for executado separadamente, o valor da variável se comportará corretamente.

Quando ocorrem impasses

Um impasse ocorre quando dois threads bloqueiam uma variável diferente ao mesmo tempo e tentam bloquear a variável que o outro thread já bloqueou. Como resultado, cada thread para de executar e aguarda que o outro thread libere a variável. Como cada thread está segurando a variável desejada pelo outro thread, nada ocorre e os threads permanecem em impasse.

Detalhes e exemplos para impasses

O código a seguir tem dois objetos LeftVal e RightVal:

  • Thread 1

    SyncLock LeftVal
        SyncLock RightVal
            'Perform operations on LeftVal and RightVal that require read and write.
        End SyncLock
    End SyncLock
    
  • Thread 2

    SyncLock RightVal
        SyncLock LeftVal
            'Perform operations on RightVal and LeftVal that require read and write.
        End SyncLock
    End SyncLock
    

Um impasse ocorre quando o Thread 1 tem permissão para bloquear LeftVal. O processador interrompe a execução do Thread 1 e inicia a execução do Thread 2. O Thread 2 bloqueia RightVal e tenta bloquear LeftVal. Como LeftVal está bloqueado, o Thread 2 para e aguarda a liberação LeftVal . Como o Thread 2 está interrompido, o Thread 1 tem permissão para continuar a execução. O Thread 1 tenta bloquear RightVal , mas não pode, porque o Thread 2 o bloqueou. Como resultado, o Thread 1 começa a aguardar até que o RightVal fique disponível. Cada thread aguarda o outro thread, pois cada thread bloqueou a variável em que o outro thread está aguardando e nenhum thread está desbloqueando a variável que ele está segurando.

Nem sempre ocorre um impasse. Se o Thread 1 executar ambos os bloqueios antes que o processador o pare, o Thread 1 poderá executar suas operações e desbloquear a variável compartilhada. Depois que o Thread 1 desbloquear a variável, o Thread 2 poderá continuar com sua execução, conforme esperado.

Esse erro parece óbvio quando esses snippets de código são colocados lado a lado, mas, na prática, o código pode aparecer em módulos ou áreas separadas do código. Esse é um erro difícil de rastrear porque, a partir desse mesmo código, pode ocorrer execução correta e execução incorreta.

Sintomas para impasses

Um sintoma comum de impasse é que o programa ou o grupo de threads para de responder. Isso também é conhecido como um enforcamento. Pelo menos dois threads estão esperando por uma variável que o outro thread bloqueou. Os threads não prosseguem, pois nenhum thread lançará sua variável até obter a outra variável. Todo o programa pode ser travado se o programa estiver aguardando em um ou em ambos os threads para concluir a execução.

O que é um thread

Os processos são usados para separar os diferentes aplicativos que estão sendo executados em um horário especificado em um único computador. O sistema operacional não executa processos, mas os threads executam. Um thread é uma unidade de execução. O sistema operacional aloca o tempo do processador em um thread para a execução das tarefas do thread. Um único processo pode conter vários threads de execução. Cada thread mantém seus próprios manipuladores de exceção, prioridades de agendamento e um conjunto de estruturas que o sistema operacional usa para salvar o contexto do thread se o thread não puder concluir sua execução durante o tempo em que foi atribuído ao processador. O contexto é mantido até a próxima vez que o thread receber a hora do processador. O contexto inclui todas as informações necessárias para que o thread continue perfeitamente sua execução. Essas informações incluem o conjunto de registros de processador do thread e a pilha de chamadas dentro do espaço de endereço do processo de host.

Referências

Para obter mais informações, pesquise Ajuda do Visual Studio para obter as seguintes palavras-chave:

  • SyncLock. Permite que um objeto seja bloqueado. Se outro thread tentar bloquear esse mesmo objeto, ele será bloqueado até que o primeiro thread seja lançado. Use SyncLock com cuidado, pois os problemas podem resultar do uso indevido do SyncLock. Por exemplo, esse comando pode impedir condições de corrida, mas causar impasses.

  • InterLocked. Permite um conjunto seleto de operações de thread-safe em variáveis numéricas básicas.

Para obter mais informações, consulte Threads e Threading.