Delen via


Debug een deadlock met behulp van de threads-weergave

Deze zelfstudie laat zien hoe u de Threads-weergave van Parallel Stacks-vensters gebruikt om fouten op te sporen in een toepassing met meerdere threads. Dit venster helpt u bij het begrijpen en controleren van het runtimegedrag van multithreaded code.

De weergave Threads wordt ondersteund voor C#, C++en Visual Basic. Voorbeeldcode is beschikbaar voor C# en C++, maar sommige codeverwijzingen en illustraties zijn alleen van toepassing op de C#-voorbeeldcode.

De weergave Threads helpt u bij het volgende:

  • Bekijk aanroepstackvisualisaties voor meerdere threads, wat een vollediger beeld biedt van de status van uw app dan het venster Oproepstack, waarin alleen de aanroepstack voor de huidige thread wordt weergegeven.

  • Identificeer problemen zoals geblokkeerde of vergrendelde threads.

Aanroepstacks met multithreaded

Identieke secties van de aanroepstack worden gegroepeerd om de visualisatie voor complexe apps te vereenvoudigen.

In de volgende conceptuele animatie ziet u hoe groepering wordt toegepast op aanroepstacks. Alleen identieke segmenten van een aanroepstack worden gegroepeerd. Beweeg de muisaanwijzer over een gegroepeerde call stack om de threads te identificeren.

Afbeelding van de groepering van call stacks.

Overzicht van voorbeeldcode (C#, C++)

De voorbeeldcode in dit scenario is bedoeld voor een toepassing die een dag in het leven van een gorilla simuleert. Het doel van de oefening is om te begrijpen hoe u de threads-weergave van het venster Parallel Stacks gebruikt om fouten op te sporen in een toepassing met meerdere threads.

Het voorbeeld bevat een voorbeeld van een impasse, die optreedt wanneer twee threads op elkaar wachten.

Om de aanroepstack intuïtief te maken, voert de voorbeeld-app de volgende opeenvolgende stappen uit:

  1. Hiermee maakt u een object dat een gorilla vertegenwoordigt.
  2. Gorilla wordt wakker.
  3. Gorilla gaat op een ochtendwandeling.
  4. Gorilla vindt bananen in de jungle.
  5. Gorilla eet.
  6. Gorilla neemt deel aan apenhandel.

Het voorbeeldproject maken

Het project maken:

  1. Open Visual Studio en maak een nieuw project.

    Als het startvenster niet is geopend, kiest u Bestand>Startvenster.

    Kies Nieuw project in het venster Start.

    In het venster Een nieuw project maken voer console in het zoekvak in. Kies vervolgens C# of C++ in de lijst Taal en kies Vervolgens Windows in de lijst Platform.

    Nadat u de taal- en platformfilters hebt toegepast, kiest u de console-app voor de gekozen taal en kiest u vervolgens Volgende.

    Note

    Als u de juiste sjabloon niet ziet, gaat u naar Tools>Get Tools and Features..., waarmee het installatieprogramma van Visual Studio wordt geopend. Kies de .NET-desktopontwikkelingswerklast en selecteer vervolgens Wijzigen.

    Typ in het venster Uw nieuwe project configureren een naam of gebruik de standaardnaam in het vak Projectnaam . Kies vervolgens Volgende.

    Kies voor een .NET-project het aanbevolen doelframework of .NET 8 en kies vervolgens Maken.

    Er verschijnt een nieuw consoleproject. Nadat het project is gemaakt, wordt er een bronbestand weergegeven.

  2. Open het codebestand .cs (of .cpp) in het project. Verwijder de inhoud om een leeg codebestand te maken.

  3. Plak de volgende code voor de gekozen taal in het lege codebestand.

     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. Selecteer in het menu Bestand de optie Alles opslaan.

  5. Selecteer in het menu BuildBuild Solution.

Gebruik de Threads-weergave in het venster Parallelle stacks

Om te beginnen met foutopsporing:

  1. Selecteer in het menu Foutopsporingstarten de foutopsporing (of F5) en wacht tot de eerste Debugger.Break() is bereikt.

    Note

    In C++wordt het foutopsporingsprogramma onderbroken in __debug_break(). De rest van de codeverwijzingen en illustraties in dit artikel zijn bedoeld voor de C#-versie, maar dezelfde principes voor foutopsporing zijn van toepassing op C++.

  2. Druk eenmaal op F5 en het foutopsporingsprogramma wordt opnieuw onderbroken op dezelfde Debugger.Break() regel.

    Dit pauzeert in de tweede aanroep naar Gorilla_Start, dat plaatsvindt in een tweede thread.

    Tip

    Het foutopsporingsprogramma wordt onderverdeeld in code per thread. Dit betekent bijvoorbeeld dat als u op F5 drukt om door te gaan met de uitvoering en de app het volgende onderbrekingspunt bereikt, deze in code op een andere thread kan inbreken. Als u dit gedrag wilt beheren voor foutopsporing, kunt u extra onderbrekingspunten, voorwaardelijke onderbrekingspunten toevoegen of Alles onderbreken gebruiken. Zie Een enkele thread volgen met voorwaardelijke onderbrekingspunten voor meer informatie over het gebruik van voorwaardelijke onderbrekingspunten.

  3. Selecteer Fouten opsporen > in Windows > Parallel Stacks om het venster Parallelle stacks te openen en selecteer vervolgens Threads in de vervolgkeuzelijst Weergave in het venster.

    Schermopname van de weergave Threads in het venster Parallelle stacks.

    In de Threads-weergave worden het stackframe en het aanroeppad van de huidige thread blauw gemarkeerd. De huidige locatie van de thread wordt weergegeven door de gele pijl.

    U ziet dat het label voor de aanroepstack Gorilla_Start is 2 threads. Toen u voor het laatst op F5 drukt, hebt u een andere thread gestart. Voor vereenvoudiging in complexe apps worden identieke aanroepstacks gegroepeerd in één visuele weergave. Dit vereenvoudigt mogelijk complexe informatie, met name in scenario's met veel threads.

    Tijdens foutopsporing kunt u in- of uitschakelen of externe code wordt weergegeven. Als u de functie wilt in- of uitschakelen, schakelt u Externe code weergeven in of uit. Als u externe code weergeeft, kunt u deze procedure nog steeds gebruiken, maar de resultaten kunnen afwijken van de illustraties.

  4. Druk nogmaals op F5 en het foutopsporingsprogramma onderbreekt de Debugger.Break() regel in de MorningWalk methode.

    In het venster Parallelle stacks ziet u de locatie van de huidige uitvoeringsthread in de MorningWalk methode.

    Schermopname van de weergave Threads na F5.

  5. Beweeg de muisaanwijzer over de MorningWalk methode om informatie op te halen over de twee threads die worden vertegenwoordigd door de gegroepeerde aanroepstack.

    Schermopname van de threads die zijn gekoppeld aan de aanroepstack.

    De huidige thread wordt ook weergegeven in de lijst Thread op de werkbalk Foutopsporing.

    Schermopname van de huidige thread op de werkbalk Foutopsporing.

    U kunt de threadlijst gebruiken om de context van het foutopsporingsprogramma over te schakelen naar een andere thread. Hiermee wordt de huidige uitvoeringsthread niet gewijzigd, alleen de context van het foutopsporingsprogramma.

    U kunt ook de context van het foutopsporingsprogramma wijzigen door te dubbelklikken op een methode in de weergave Threads of door met de rechtermuisknop op een methode te klikken in de weergave Threads en Overschakelen naar Frame>[thread-id] te selecteren.

  6. Druk nogmaals op F5 en de debugger pauzeert bij de MorningWalk-methode voor de tweede thread.

    Schermopname van de weergave Threads na tweede F5.

    Afhankelijk van het uitvoeringsmoment van threads ziet u op dat moment afzonderlijke of gegroepeerde aanroepstacks.

    In de voorgaande afbeelding zijn de aanroepstacks voor de twee threads gedeeltelijk gegroepeerd. De identieke segmenten van de aanroepstacks worden gegroepeerd en pijllijnen wijzen naar de segmenten die zijn gescheiden (dat wil gezegd, niet identiek). Het huidige stackframe wordt aangegeven met de blauwe markering.

  7. Druk nogmaals op F5 en u ziet een lange vertraging en in de weergave Threads worden geen gespreksstackgegevens weergegeven.

    De vertraging wordt veroorzaakt door een impasse. Er wordt niets weergegeven in de weergave Threads, omdat u momenteel niet gepauzeerd bent in de debugger, hoewel threads mogelijk worden geblokkeerd.

    Note

    In C++ziet u ook een foutopsporingsfout die aangeeft dat abort() is aangeroepen.

    Tip

    De knop Break All is een goede manier om callstack-informatie op te halen als er een deadlock optreedt of als alle threads momenteel geblokkeerd zijn.

  8. Selecteer bovenaan de IDE in de werkbalk Foutopsporing de knop Alles onderbreken (pauzepictogram) of gebruik Ctrl + Alt + Break.

    Schermopname van de weergave Threads na het selecteren van Alles verbreken.

    Bovenaan de aanroepstack in de weergave Threads ziet u dat FindBananas vastgelopen is. De uitvoeringswijzer in FindBananas is een gekrulde groene pijl, die de huidige foutopsporingscontext aangeeft, en tevens dat de threads momenteel niet actief zijn.

    Note

    In C++ziet u de nuttige informatie en pictogrammen over de gedetecteerde impasse niet. U vindt echter nog steeds de gekrulde groene pijl in Jungle.FindBananas, die wijst op de locatie van de impasse.

    In de code-editor vinden we de gekrulde groene pijl in de lock functie. De twee threads worden geblokkeerd op de lock functie in de FindBananas methode.

    Schermopname van de code-editor nadat u Alles verbreken hebt geselecteerd.

    Afhankelijk van de volgorde van threaduitvoering wordt de impasse weergegeven in de lock(tree) of lock(banana_bunch) instructie.

    De aanroep naar lock blokkeert de threads in de FindBananas methode. De ene thread wacht totdat de vergrendeling tree wordt vrijgegeven door de andere thread, maar de andere thread wacht tot de banana_bunch vergrendeling wordt vrijgegeven voordat de vergrendeling treekan worden losgelaten. Dit is een voorbeeld van een klassieke impasse die optreedt wanneer twee threads op elkaar wachten.

    Als u Copilot gebruikt, kunt u ook samenvattingen van door AI gegenereerde threads ophalen om potentiële impasses te identificeren.

    Schermopname van overzichtsbeschrijvingen van Copilot-threads.

De voorbeeldcode herstellen

U kunt deze code oplossen door altijd meerdere vergrendelingen te verkrijgen in een consistente, globale volgorde voor alle threads. Dit voorkomt ronde wachttijden en elimineert impasses.

  1. Om de impasse op te lossen, vervang de code in MorningWalk door de volgende code.

    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. Start de app opnieuw op.

Summary

In deze walkthrough is het debugger-venster Parallel Stacks gedemonstreerd. Gebruik dit venster voor echte projecten die multithreaded code gebruiken. U kunt parallelle code onderzoeken die is geschreven in C++, C# of Visual Basic.