Udostępnij za pomocą


Debugowanie zakleszczenia przy użyciu widoku Wątki

W tym samouczku pokazano, jak debugować aplikację wielowątkową za pomocą widoku Wątki okien stosów równoległych . To okno pomaga zrozumieć i zweryfikować zachowanie w czasie wykonywania kodu wielowątkowego.

Widok Wątki jest obsługiwany dla języków C#, C++i Visual Basic. Przykładowy kod jest dostarczany dla języków C# i C++, ale niektóre odwołania do kodu i ilustracje mają zastosowanie tylko do przykładowego kodu w języku C#.

Widok Wątków ułatwia:

  • Wyświetl wizualizacje stosu wywołań dla wielu wątków, które zapewniają bardziej pełny obraz stanu aplikacji niż okno stosu wywołań, które pokazuje stos wywołań dla bieżącego wątku.

  • Pomoc w identyfikowaniu problemów, takich jak zablokowane lub zakleszczone wątki.

Wielowątkowane stosy wywołań

Identyczne sekcje stosu wywołań są grupowane razem, aby uprościć wizualizację złożonych aplikacji.

Poniższa animacja koncepcyjna pokazuje, jak stosuje się grupowanie do stosów wywołań. Grupowane są tylko identyczne segmenty stosu wywołań. Umieść kursor na zgrupowanym stosie wywołań, aby idenitfy wątków.

Ilustracja przedstawiająca grupowanie stosów wywołań.

Omówienie przykładowego kodu (C#, C++)

Przykładowy kod w tym przewodniku dotyczy aplikacji, która symuluje dzień w życiu goryla. Celem ćwiczenia jest zrozumienie, jak używać widoku Wątki w oknie stosów równoległych do debugowania aplikacji wielowątkowej.

Przykład zawiera przykład zakleszczenia, który występuje, gdy na siebie czekają dwa wątki.

Aby stos wywołań był intuicyjny, przykładowa aplikacja wykonuje następujące kroki sekwencyjne:

  1. Tworzy obiekt reprezentujący goryl.
  2. Gorilla budzi się.
  3. Gorilla idzie na poranny spacer.
  4. Gorilla znajduje banany w dżungli.
  5. Gorilla zjada.
  6. Gorilla angażuje się w biznes małpy.

Tworzenie przykładowego projektu

Aby utworzyć projekt:

  1. Otwórz program Visual Studio i utwórz nowy projekt.

    Jeśli okno startowe nie jest otwarte, wybierz Plik>Okno startowe.

    W oknie Start wybierz pozycję Nowy projekt.

    W oknie Tworzenie nowego projektu wprowadź lub wpisz konsolę w polu wyszukiwania. Następnie wybierz pozycję C# lub C++ z listy Język, a następnie wybierz pozycję Windows z listy Platforma.

    Po zastosowaniu filtrów języka i platformy wybierz aplikację konsolową dla wybranego języka, a następnie wybierz przycisk Dalej.

    Note

    Jeśli nie widzisz poprawnego szablonu, przejdź do pozycji NarzędziaPobierz narzędzia >i funkcje..., co spowoduje otwarcie Instalatora programu Visual Studio. Wybierz obciążenie programowanie aplikacji .NET na komputerach stacjonarnych, a następnie wybierz Modyfikuj.

    W oknie Konfigurowanie nowego projektu wpisz nazwę lub użyj nazwy domyślnej w polu Nazwa projektu . Następnie wybierz pozycję Dalej.

    W przypadku projektu platformy .NET wybierz zalecaną strukturę docelową lub platformę .NET 8, a następnie wybierz pozycję Utwórz.

    Pojawia się nowy projekt konsoli. Po utworzeniu projektu zostanie wyświetlony plik źródłowy.

  2. Otwórz plik kodu .cs (lub .cpp) w projekcie. Usuń jego zawartość, aby utworzyć pusty plik kodu.

  3. Wklej następujący kod wybranego języka do pustego pliku kodu.

     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. W menu Plik wybierz pozycję Zapisz wszystko.

  5. W menu Build wybierz pozycję Build Solution.

Użyj widoku Wątki w oknie stosów równoległych

Aby rozpocząć debugowanie:

  1. W menu Debugowanie wybierz pozycję Rozpocznij debugowanie (lub F5) i poczekaj na pierwsze Debugger.Break() trafienie.

    Note

    W języku C++debuger wstrzymuje się w pliku __debug_break(). Pozostałe odwołania do kodu i ilustracje w tym artykule dotyczą wersji języka C#, ale te same zasady debugowania dotyczą języka C++.

  2. Naciśnij F5 raz, a debuger ponownie wstrzymuje się w tym samym Debugger.Break() wierszu.

    Spowoduje to wstrzymanie w drugim wywołaniu metody Gorilla_Start, która występuje w drugim wątku.

    Tip

    Debuger dzieli kod na każdy wątek. Na przykład oznacza to, że jeśli naciskasz F5, aby kontynuować wykonywanie, a aplikacja napotka następny punkt przerwania, może wejść w kod w innym wątku. Jeśli chcesz zarządzać tym zachowaniem w celach debugowania, możesz dodać dodatkowe punkty przerwania, warunkowe punkty przerwania lub użyć opcji Break All. Aby uzyskać więcej informacji na temat używania warunkowych punktów przerwania, zobacz Obserwowanie pojedynczego wątku z warunkowymi punktami przerwania.

  3. Wybierz pozycję Debuguj > stosy równoległe systemu Windows>, aby otworzyć okno Stosy równoległe, a następnie wybierz pozycję Wątki z listy rozwijanej Widok w oknie.

    Zrzut ekranu przedstawiający Widok Wątków w oknie Parallel Stacks.

    W widoku wątków ramka stosu i ścieżka wywołania bieżącego wątku są zaznaczone na niebiesko. Bieżąca lokalizacja wątku jest wyświetlana za pomocą żółtej strzałki.

    Zwróć uwagę, że etykieta stosu wywołań dla Gorilla_Start to 2 Wątki. Po ostatnim naciśnięciu F5 rozpoczęto kolejny wątek. W celu uproszczenia złożonych aplikacji identyczne stosy wywołań są grupowane w jedną wizualną reprezentację. Upraszcza to potencjalnie złożone informacje, szczególnie w scenariuszach z wieloma wątkami.

    Podczas debugowania można przełączać, czy kod zewnętrzny jest wyświetlany. Aby przełączyć tę funkcję, zaznacz lub odznacz opcję Pokaż kod zewnętrzny. Jeśli pokażesz kod zewnętrzny, nadal możesz użyć tego przewodnika, ale wyniki mogą się różnić od ilustracji.

  4. Naciśnij F5 ponownie, a debuger wstrzymuje się w Debugger.Break() wierszu w metodzie MorningWalk .

    W oknie Stosy równoległe jest wyświetlana lokalizacja bieżącego wątku wykonującego w metodzie MorningWalk .

    Zrzut ekranu przedstawiający widok Wątki po odświeżeniu za pomocą klawisza F5.

  5. Umieść kursor na metodzie MorningWalk, aby uzyskać informacje o dwóch wątkach reprezentowanych przez zgrupowany stos wywołań.

    Zrzut ekranu przedstawiający wątki skojarzone ze stosem wywołań.

    Bieżący wątek jest również wyświetlany na liście Wątki na pasku narzędzi Debugowanie.

    Zrzut ekranu przedstawiający bieżący wątek na pasku narzędzi Debugowanie.

    Możesz użyć listy Wątki , aby przełączyć kontekst debugera na inny wątek. Nie powoduje to zmiany bieżącego wątku wykonywania, lecz wyłącznie kontekstu debugera.

    Alternatywnie możesz przełączyć kontekst debugera, klikając dwukrotnie metodę w widoku Wątki lub klikając prawym przyciskiem myszy metodę w widoku Wątki i wybierając pozycję Przełącz na ramkę>[identyfikator wątku].

  6. Naciśnij F5 ponownie, a debuger wstrzymuje metodę MorningWalk dla drugiego wątku.

    Zrzut ekranu przedstawiający widok Wątki po drugim F5.

    W zależności od czasu wykonywania wątku w tym momencie zobaczysz oddzielne lub zgrupowane stosy wywołań.

    Na poprzedniej ilustracji stosy wywołań dla dwóch wątków są częściowo zgrupowane. Identyczne segmenty stosów wywołań są grupowane, a linie strzałek wskazują segmenty rozdzielone (czyli nie identyczne). Bieżąca ramka stosu jest oznaczona niebieskim podświetleniem.

  7. Naciśnij ponownie F5, a zauważysz duże opóźnienie. W widoku wątków nie są wyświetlane żadne informacje o stosie wywołań.

    Opóźnienie jest spowodowane zakleszczeniem. Nic nie jest wyświetlane w widoku Wątki, ponieważ mimo że wątki mogą być zablokowane, nie są obecnie wstrzymane w debugerze.

    Note

    W języku C++zostanie również wyświetlony błąd debugowania wskazujący, że abort() został wywołany.

    Tip

    Przycisk Przerwij wszystko jest dobrym sposobem na uzyskanie informacji o stosie wywołań, jeśli wystąpi zakleszczenie lub wszystkie wątki są obecnie zablokowane.

  8. W górnej części środowiska IDE na pasku narzędzi Debugowanie wybierz przycisk Przerwij wszystko (ikona wstrzymywania) lub naciśnij Ctrl + Alt + Break.

    Zrzut ekranu przedstawiający widok Threads po wybraniu opcji Break All.

    Górna część stosu wywołań w widoku Wątki pokazuje, że FindBananas jest zakleszczone. Wskaźnik wykonywania w elemencie FindBananas jest zwiniętą zieloną strzałką, która wskazuje bieżący kontekst debugera. Ponadto informuje nas, że wątki nie są obecnie uruchomione.

    Note

    W języku C++nie widzisz przydatnych informacji i ikon "zakleszczenia". Jednak nadal znajdziesz zwiniętą zieloną strzałkę w Jungle.FindBananaspliku , wskazując na lokalizację zakleszczenia.

    W edytorze kodu znajdujemy zwiniętą zieloną strzałkę w lock funkcji. Dwa wątki są blokowane na funkcji lock w metodzie FindBananas.

    Zrzut ekranu edytora kodu po wybraniu pozycji Przerwij wszystko.

    W zależności od kolejności wykonywania wątków, zakleszczenie pojawia się w instrukcji lock(tree) lub lock(banana_bunch).

    Wywołanie metody lock blokuje wątki w metodzie FindBananas . Jeden wątek czeka na zwolnienie blokady tree przez drugi wątek, ale drugi wątek czeka na zwolnienie blokady banana_bunch , zanim będzie mógł zwolnić blokadę na tree. Jest to przykład klasycznego zakleszczenia, który występuje, gdy dwa wątki czekają na siebie nawzajem.

    Jeśli używasz narzędzia Copilot, możesz również uzyskać podsumowania wątków generowanych przez sztuczną inteligencję, aby ułatwić zidentyfikowanie potencjalnych zakleszczeń.

    Zrzut ekranu przedstawiający opisy podsumowań wątków Copilot.

Naprawianie przykładowego kodu

Aby naprawić ten kod, zawsze uzyskuj wiele blokad w spójnym, globalnym porządku we wszystkich wątkach. Zapobiega to kolistym oczekiwaniom i eliminuje zakleszczenia.

  1. Aby naprawić zakleszczenie, zastąp kod kodem znajdującym się poniżej MorningWalk.

    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. Uruchom ponownie aplikację.

Summary

W tym poradniku pokazano okno debugera stosów równoległych. Użyj tego okna w rzeczywistych projektach, które używają kodu wielowątkowego. Możesz zbadać kod równoległy napisany w języku C++, C# lub Visual Basic.