Compartilhar via


Depurar um deadlock usando a exibição Threads

Este tutorial mostra como usar o modo de exibição Threads das janelas pilhas paralelas para depurar um aplicativo multithreaded. Essa janela ajuda você a entender e verificar o comportamento em tempo de execução do código multithreaded.

O modo de exibição Threads tem suporte para C#, C++e Visual Basic. O código de exemplo é fornecido para C# e C++, mas algumas das referências de código e ilustrações se aplicam somente ao código de exemplo C#.

A visualização de Threads ajuda você a:

  • Exiba visualizações de pilha de chamadas para vários threads, o que fornece uma imagem mais completa do estado do aplicativo do que a janela Pilha de Chamadas, que mostra apenas a pilha de chamadas para o thread atual.

  • Ajudar a identificar problemas como threads bloqueadas ou em deadlocks.

Pilhas de chamadas multithread

Seções idênticas da pilha de chamadas são agrupadas para simplificar a visualização de aplicativos complexos.

A animação conceitual a seguir mostra como o agrupamento é aplicado a pilhas de chamadas. Somente segmentos idênticos de uma pilha de chamadas são agrupados. Passe o mouse sobre uma pilha de chamadas agrupada para identificar os threads.

Ilustração do agrupamento de pilhas de chamadas.

Visão geral do código de exemplo (C#, C++)

O código de exemplo neste passo a passo é para um aplicativo que simula um dia na vida de um gorila. O objetivo do exercício é entender como usar a exibição Threads da janela Pilhas Paralelas para depurar um aplicativo multithread.

A amostra inclui um exemplo de deadlock, que ocorre quando duas threads estão esperando uma pela outra.

Para tornar a pilha de chamadas intuitiva, o aplicativo de exemplo executa as seguintes etapas sequenciais:

  1. Cria um objeto que representa um gorila.
  2. Gorila acorda.
  3. Gorila vai em uma caminhada matinal.
  4. Gorila encontra bananas na selva.
  5. Gorila come.
  6. Gorila se envolve em negócios de macacos.

Criar o projeto de exemplo

Para criar o projeto:

  1. Abra o Visual Studio e crie um novo projeto.

    Se a janela inicial não estiver aberta, escolha Arquivo>Janela de Início.

    Na janela Iniciar, escolha Novo projeto.

    Na janela Criar um novo projeto , insira ou digite o console na caixa de pesquisa. Em seguida, escolha C# ou C++ na lista de idiomas e escolha o Windows na lista plataforma.

    Depois de aplicar os filtros de linguagem e plataforma, escolha o Aplicativo de Console para o idioma escolhido e escolha Avançar.

    Note

    Se você não vir o modelo correto, vá para Ferramentas>Obter Ferramentas e Recursos..., o que abre o Instalador do Visual Studio. Escolha a carga de trabalho Desenvolvimento de área de trabalho do .NET e, em seguida, selecione Modificar.

    Na janela Configurar seu novo projeto, digite um nome ou use o nome padrão na caixa de nome do Projeto . Em seguida, escolha Avançar.

    Para um projeto .NET, escolha a estrutura de destino recomendada ou o .NET 8 e, em seguida, escolha Criar.

    Um novo projeto de console aparece. Depois que o projeto for criado, um arquivo de origem será exibido.

  2. Abra o arquivo de código .cs (ou .cpp) no projeto. Exclua seu conteúdo para criar um arquivo de código vazio.

  3. Cole o código a seguir para o idioma escolhido no arquivo de código vazio.

     using System.Diagnostics;
    
     namespace Multithreaded_Deadlock
     {
         class Jungle
         {
             public static readonly object tree = new object();
             public static readonly object banana_bunch = new object();
             public static Barrier barrier = new Barrier(2);
    
             public static int FindBananas()
             {
                 // Lock tree first, then banana
                 lock (tree)
                 {
                     lock (banana_bunch)
                     {
                         Console.WriteLine("Got bananas.");
                         return 0;
                     }
                 }
             }
    
             static void Gorilla_Start(object lockOrderObj)
             {
                 Debugger.Break();
                 bool lockTreeFirst = (bool)lockOrderObj;
                 Gorilla koko = new Gorilla(lockTreeFirst);
                 int result = 0;
                 var done = new ManualResetEventSlim(false);
    
                 Thread t = new Thread(() =>
                 {
                     result = koko.WakeUp();
                     done.Set();
                 });
                 t.Start();
                 done.Wait();
             }
    
             static void Main(string[] args)
             {
                 List<Thread> threads = new List<Thread>();
                 // Start two threads with opposite lock orders
                 threads.Add(new Thread(Gorilla_Start));
                 threads[0].Start(true);  // First gorilla locks tree then banana
                 threads.Add(new Thread(Gorilla_Start));
                 threads[1].Start(false); // Second gorilla locks banana then tree
    
                 foreach (var t in threads)
                 {
                     t.Join();
                 }
             }
         }
    
         class Gorilla
         {
             private readonly bool lockTreeFirst;
    
             public Gorilla(bool lockTreeFirst)
             {
                 this.lockTreeFirst = lockTreeFirst;
             }
    
             public int WakeUp()
             {
                 int myResult = MorningWalk();
                 return myResult;
             }
    
             public int MorningWalk()
             {
                 Debugger.Break();
                 if (lockTreeFirst)
                 {
                     lock (Jungle.tree)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 else
                 {
                     lock (Jungle.banana_bunch)
                     {
                         Jungle.barrier.SignalAndWait(5000); // For thread timing consistency in sample
                         Jungle.FindBananas();
                         GobbleUpBananas();
                     }
                 }
                 return 0;
             }
    
             public void GobbleUpBananas()
             {
                 Console.WriteLine("Trying to gobble up food...");
                 DoSomeMonkeyBusiness();
             }
    
             public void DoSomeMonkeyBusiness()
             {
                 Thread.Sleep(1000);
                 Console.WriteLine("Monkey business done");
             }
         }
     }
    
  4. No menu Arquivo, selecione Salvar Tudo.

  5. No menu Build, selecione Compilar Solução.

Use a exibição Threads da janela Pilhas Paralelas

Para iniciar a depuração:

  1. No menu Depurar , selecione Iniciar Depuração (ou F5) e aguarde até que o primeiro Debugger.Break() seja atingido.

    Note

    No C++, o depurador pausa em __debug_break(). O restante das referências de código e ilustrações neste artigo são para a versão C#, mas os mesmos princípios de depuração se aplicam ao C++.

  2. Pressione F5 uma vez e o depurador pausa novamente na mesma Debugger.Break() linha.

    Isso pausa na segunda chamada para Gorilla_Start, que ocorre dentro de outro thread.

    Tip

    O depurador divide o código por thread. Por exemplo, isso significa que, se você pressionar F5 para continuar a execução e o aplicativo atingir o próximo ponto de interrupção, ele poderá dividir o código em um thread diferente. Se você precisar gerenciar esse comportamento para fins de depuração, poderá adicionar pontos de interrupção adicionais, pontos de interrupção condicionais ou usar Break All. Para obter mais informações sobre como usar pontos de interrupção condicionais, consulte Seguir um único thread com pontos de interrupção condicionais.

  3. Selecione Debug > Windows > Pilhas Paralelas para abrir a janela Pilhas Paralelas e selecione Threads no menu suspenso Exibir na janela.

    Captura de tela da visualização Threads na janela Pilhas Paralelas.

    Na exibição Threads, o registro de ativação e o caminho de chamada do thread atual são realçados em azul. O local atual do thread é mostrado pela seta amarela.

    Observe que o rótulo da pilha de chamadas para Gorilla_Start é 2 Threads. Quando você pressionou F5 pela última vez, você iniciou outro thread. Para simplificação em aplicativos complexos, pilhas de chamadas idênticas são agrupadas em uma única representação visual. Isso simplifica informações potencialmente complexas, especialmente em cenários com muitos threads.

    Durante a depuração, você pode alternar se o código externo é exibido. Para alternar o recurso, selecione ou remova a seleção de Mostrar Código Externo. Se você mostrar código externo, ainda poderá usar este passo a passo, mas seus resultados podem ser diferentes das ilustrações.

  4. Pressione F5 novamente e o depurador pausa na linha Debugger.Break() dentro do método MorningWalk.

    A janela Pilhas Paralelas mostra a localização do thread de execução atual dentro do método MorningWalk.

    Captura de tela do modo de exibição Threads após F5.

  5. Passe o mouse sobre o método MorningWalk para obter informações sobre os dois threads representados pela pilha de chamadas agrupada.

    Captura de tela dos threads associados à pilha de chamadas.

    O thread atual também aparece na lista Thread na barra de ferramentas de depuração.

    Captura de tela do tópico atual na barra de ferramentas de depuração.

    Você pode usar a lista Thread para alternar o contexto do depurador para um thread diferente. Isso não altera o thread de execução atual, apenas o contexto do depurador.

    Como alternativa, você pode alternar o contexto do depurador clicando duas vezes em um método no modo de exibição Threads ou clicando com o botão direito do mouse em um método na exibição Threads e selecionando Alternar para Quadro>[ID do thread].

  6. Pressione F5 novamente e o depurador pausa no método MorningWalk para o segundo thread.

    Captura de tela do modo de exibição Threads após o segundo F5.

    Dependendo do tempo de execução do thread, neste ponto você verá pilhas de chamadas separadas ou agrupadas.

    Na ilustração anterior, as pilhas de chamadas dos dois threads estão parcialmente agrupadas. Os segmentos idênticos das pilhas de chamadas são agrupados e as linhas de seta apontam para os segmentos separados (ou seja, não idênticos). O quadro de pilha atual é indicado pelo destaque azul.

  7. Pressione F5 novamente e você verá um longo atraso, e a visão de Threads não mostrará nenhuma informação de pilha de chamadas.

    O atraso é causado por um deadlock. Nada aparece na exibição Threads porque, embora os threads possam estar bloqueados, você não está pausado no depurador no momento.

    Note

    No C++, você também vê um erro de depuração indicando que abort() foi chamado.

    Tip

    O botão Interromper Tudo é uma boa maneira de obter informações de pilha de chamadas se ocorrer um deadlock ou todos os threads estiverem bloqueados no momento.

  8. Na parte superior do IDE na barra de ferramentas Depurar, selecione o botão Interromper Tudo (ícone de pausa) ou use Ctrl + Alt + Break.

    Captura de tela da visualização Threads depois de selecionar Interromper Tudo.

    A parte superior da pilha de chamadas na visualização de Threads mostra que FindBananas está em deadlock. O ponteiro de execução em FindBananas é uma seta verde ondulada, indicando o contexto atual do depurador, além de informar que as threads não estão em execução no momento.

    Note

    No C++, você não vê as informações e ícones úteis de "deadlock detectado". No entanto, você ainda encontra a seta Jungle.FindBananasverde enrolada, sugerindo o local do deadlock.

    No editor de código, encontramos a seta verde ondulada na lock função. As duas threads estão bloqueadas na função lock no método FindBananas.

    Captura de tela do editor de código depois de selecionar Interromper Tudo.

    Dependendo da ordem de execução da linha de execução, o deadlock aparece na instrução lock(tree) ou na instrução lock(banana_bunch).

    A chamada para lock bloqueia os threads no método FindBananas. Um thread está aguardando o bloqueio tree ser liberado pelo outro thread, mas o outro thread está aguardando o bloqueio banana_bunch ser liberado antes que ele possa liberar o bloqueio tree. Este é um exemplo de um deadlock clássico que ocorre quando dois threads estão esperando um pelo outro.

    Se você estiver usando o Copilot, também poderá obter resumos de thread gerados por IA para ajudar a identificar possíveis deadlocks.

    Captura de tela das descrições resumidas do tópico do Copilot.

Corrigir o código de exemplo

Para corrigir esse código, sempre adquira vários bloqueios em uma ordem global e consistente em todos os threads. Isso impede esperas circulares e elimina deadlocks.

  1. Para corrigir o deadlock, substitua o código MorningWalk pelo código a seguir.

    public int MorningWalk()
    {
        Debugger.Break();
        // Always lock tree first, then banana_bunch
        lock (Jungle.tree)
        {
            Jungle.barrier.SignalAndWait(5000); // OK to remove
            lock (Jungle.banana_bunch)
            {
                Jungle.FindBananas();
                GobbleUpBananas();
            }
        }
        return 0;
    }
    
  2. Reinicie o aplicativo.

Summary

Este passo a passo demonstrou a janela do depurador Pilhas Paralelas. Use essa janela em projetos reais que usam código multithreaded. Você pode examinar o código paralelo escrito em C++, C#ou Visual Basic.