Partilhar via


Depurar um deadlock usando a vista Threads

Este tutorial mostra como usar a visualização Threads de janelas Parallel Stacks para depurar um aplicativo multithreaded. Esta janela ajuda você a entender e verificar o comportamento em tempo de execução do código multithreaded.

O modo de exibição Threads é suportado 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 apenas ao código de exemplo C#.

A vista Tópicos ajuda-o a:

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

  • Ajude a identificar problemas como threads bloqueadas ou em impasse.

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. Apenas segmentos idênticos de uma pilha de chamadas são agrupados. Passe o cursor sobre uma pilha de chamadas agrupadas para identificar os threads.

Ilustração do agrupamento das 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 visualização Threads da janela Parallel Stacks para depurar um aplicativo multithreaded.

O exemplo inclui um exemplo de deadlock, que ocorre quando dois processos estão à espera um do outro.

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 de início não estiver aberta, escolha Arquivo>Janela de Início.

    Na janela Iniciar, escolha Novo projeto.

    Na janela Criar um novo projeto , digite ou digite console na caixa de pesquisa. Em seguida, escolha C# ou C++ na lista Idioma e, em seguida, escolha Windows na lista Plataforma .

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

    Note

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

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

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

    Um novo projeto de console é exibido. 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 Compilação, selecione Compilar Solução.

Usar o modo de 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

    Em 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 uma segunda thread.

    Tip

    O depurador quebra em 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á quebrar 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 Quebrar tudo. 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 Depurar > Windows > Pilhas Paralelas para abrir a janela Pilhas Paralelas e, em seguida, selecione Threads na lista pendente Exibir na janela.

    Captura de ecrã da vista Threads na janela Parallel Stacks.

    Na visualização Threads, o frame de pilha e o caminho da chamada do thread atual são realçados em azul. A localização atual do segmento é mostrada pela seta amarela.

    Observe que o rótulo para a pilha de chamadas de 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 desmarque 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() no método MorningWalk.

    A janela Pilhas paralelas mostra o local do thread de execução atual no MorningWalk método.

    Captura de ecrã da vista Threads após F5.

  5. Mova o cursor sobre o MorningWalk método para obter informações sobre os dois encadeamentos representados pela pilha de chamadas em grupo.

    Captura de ecrã dos threads associados à pilha de chamadas.

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

    Captura de ecrã do thread 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 na visualização Threads ou clicando com o botão direito do mouse em um método na visualização Threads e selecionando Alternar para Frame>[thread ID].

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

    Captura de ecrã da vista 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 para os dois threads sã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 frame atual da stack é indicado pelo realce azul.

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

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

    Note

    Em 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 da pilha de chamadas se ocorrer um impasse ou se todos os threads estiverem bloqueados naquele momento.

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

    Captura de ecrã da vista Threads depois de selecionar Quebrar tudo.

    A parte superior da pilha de chamadas na vista Threads mostra que FindBananas está em deadlock. O ponteiro de execução em FindBananas é uma seta verde curva, indicando o contexto atual do depurador, mas também nos diz que os threads não estão atualmente em execução.

    Note

    Em C++, você não vê as informações e ícones úteis de "deadlock detetado". No entanto, você ainda encontra a seta verde enrolada em Jungle.FindBananas, sugerindo a localização do impasse.

    No editor de código, encontramos a seta verde curva na função lock. Os dois threads estão bloqueados na lock função no FindBananas método.

    Captura de tela do editor de código depois de selecionar Quebrar tudo.

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

    A chamada para lock bloqueia os threads no FindBananas método. Uma thread está esperando que o bloqueio em tree seja liberado pelo outro thread, mas o outro thread está esperando que o bloqueio em banana_bunch seja liberado antes de poder liberar o bloqueio em tree. Este é um exemplo de um impasse clássico que ocorre quando dois encadeamentos estão à espera um do outro.

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

    Captura de tela das descrições de resumo do thread Copilot.

Corrigir o código de exemplo

Para corrigir este código, adquira sempre bloqueios múltiplos numa ordem global consistente em todas as threads. Isso evita esperas circulares e elimina impasses.

  1. Para corrigir o deadlock, substitua o código em MorningWalk pelo código seguinte.

    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

Neste passo a passo, foi demonstrada a janela do depurador Parallel Stacks. Use esta janela em projetos reais que usam código multithreaded. Você pode examinar código paralelo escrito em C++, C# ou Visual Basic.