Sdílet prostřednictvím


Ladění vzájemného zablokování pomocí zobrazení Vláken

V tomto kurzu se dozvíte, jak pomocí zobrazení Vlákenoken Paralelní zásobníky ladit vícevláknovou aplikaci. Toto okno vám pomůže pochopit a ověřit chování vícevláknového kódu za běhu.

Zobrazení vláken je podporováno pro C#, C++ a Visual Basic. Vzorový kód je k dispozici pro C# a C++, ale některé odkazy na kód a ilustrace platí jenom pro ukázkový kód jazyka C#.

Pohled na vlákna vám pomůže:

  • Zobrazení vizualizací zásobníku volání pro více vláken, které poskytují ucelenější obrázek stavu aplikace než okno Zásobník volání, které zobrazuje zásobník volání pro aktuální vlákno.

  • Pomoc s identifikací problémů, jako jsou blokovaná nebo zablokovaná vlákna

Zásobníky vícevláknových volání

Stejné části zásobníku volání jsou seskupené dohromady, aby se zjednodušila vizualizace složitých aplikací.

Následující konceptuální animace ukazuje, jak se seskupování aplikuje na zásobníky volání. Seskupují se pouze identické segmenty zásobníku volání. Najeďte myší na seskupený zásobník volání, aby bylo vlákno idenitfy.

Obrázek seskupení zásobníků volání

Přehled ukázkového kódu (C#, C++)

Vzorový kód v tomto návodu je určen pro aplikaci, která simuluje den v životě gorily. Účelem tohoto cvičení je pochopit, jak pomocí zobrazení Vláken okna Paralelní zásobníky ladit vícevláknovou aplikaci.

Ukázka obsahuje příklad zablokování, ke kterému dochází, když na sebe čekají dvě vlákna.

Aby byl zásobník volání intuitivní, ukázková aplikace provede následující postupné kroky:

  1. Vytvoří objekt představující gorilu.
  2. Gorilla se vzbudí.
  3. Gorilla chodí ráno.
  4. Gorilla najde banány v džungli.
  5. Gorilla jí.
  6. Gorilla se zabývá opičím obchodem.

Vytvoření ukázkového projektu

Vytvoření projektu:

  1. Otevřete Visual Studio a vytvořte nový projekt.

    Pokud úvodní okno není otevřené, zvolte Soubor>Okno Start.

    V okně Start zvolte Nový projekt.

    V okně Vytvořit nový projekt zadejte konzolu do vyhledávacího pole. Potom v seznamu Jazyků zvolte C# nebo C++ a pak ze seznamu Platformy zvolte Windows .

    Po použití filtrů jazyka a platformy zvolte konzolovou aplikaci pro vybraný jazyk a pak zvolte Další.

    Note

    Pokud nevidíte správnou šablonu, přejděte na NástrojeZískat nástroje >a funkce..., čímž se otevře instalační program sady Visual Studio. Zvolte úlohu vývoje desktopových aplikací .NET a pak zvolte Upravit.

    V okně Konfigurovat nový projekt zadejte název nebo do pole Název projektu použijte výchozí název. Pak zvolte Další.

    V případě projektu .NET zvolte buď doporučenou cílovou architekturu, nebo .NET 8, a pak zvolte Vytvořit.

    Zobrazí se nový konzolový projekt. Po vytvoření projektu se zobrazí zdrojový soubor.

  2. Otevřete soubor kódu .cs (nebo .cpp) v projektu. Odstraňte jeho obsah a vytvořte prázdný soubor kódu.

  3. Do prázdného souboru kódu vložte následující kód pro vybraný jazyk.

     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. V nabídce Soubor vyberte Uložit vše.

  5. V nabídce Sestavení vyberte Sestavit řešení.

Použití zobrazení Vláken okna Paralelní zásobníky

Chcete-li začít ladit:

  1. V nabídce Ladění vyberte Spustit ladění (nebo F5) a počkejte, až se spustí první Debugger.Break() .

    Note

    V jazyce C++ se ladicí program pozastaví v __debug_break(). Zbývající odkazy na kód a ilustrace v tomto článku jsou určené pro verzi jazyka C#, ale stejné principy ladění platí pro jazyk C++.

  2. Stiskněte jednou klávesu F5 a ladicí program se znovu pozastaví na stejném Debugger.Break() řádku.

    Tím se pozastaví druhé volání Gorilla_Start, které se vyskytuje v druhém vlákně.

    Tip

    Ladicí program se rozdělí na kód na základě jednotlivých vláken. To například znamená, že pokud stisknete F5 pro pokračování v provádění a aplikace narazí na další bod přerušení, může se zastavit v kódu na jiném vlákně. Pokud potřebujete toto chování spravovat pro účely ladění, můžete přidat další zarážky, podmíněné zarážky nebo použít funkci Přerušit vše. Více informací o používání podmíněných zarážek najdete v tématu Sledování jednoho vlákna s podmíněnými zarážkami.

  3. Výběrem možnosti Ladit > paralelní zásobníky systému Windows > otevřete okno Paralelní zásobníky a potom v rozevíracím seznamu Zobrazení vyberte Vlákna.

    Snímek obrazovky zobrazení Vláken v okně Paralelní zásobníky

    V zobrazení vláken jsou zásobníkový rám a cesta volání aktuálního vlákna zvýrazněny modře. Aktuální umístění vlákna je zobrazeno žlutou šipkou.

    Všimněte si označení zásobníku volání pro 2 vlákna. Když jste naposledy stiskli klávesu F5, spustili jste další vlákno. Pro zjednodušení v složitých aplikacích jsou identické zásobníky volání seskupené do jediné vizuální reprezentace. To zjednodušuje potenciálně složité informace, zejména ve scénářích s mnoha vlákny.

    Během ladění můžete přepnout, jestli se zobrazí externí kód. Pokud chcete tuto funkci přepnout, vyberte nebo zrušte zaškrtnutí políčka Zobrazit externí kód. Pokud zobrazíte externí kód, můžete tento názorný postup použít, ale výsledky se můžou lišit od ilustrací.

  4. Stiskněte znovu klávesu F5 a ladicí program se pozastaví na Debugger.Break() řádku v MorningWalk metodě.

    Okno Paralelních zásobníků zobrazuje umístění aktuálně spuštěného vlákna v metodě MorningWalk.

    Snímek obrazovky se zobrazením vláken za F5

  5. Najeďte myší na metodu MorningWalk a získejte informace o dvou vláknech reprezentovaných seskupeným zásobníkem volání.

    Snímek obrazovky zobrazující vlákna spojená se zásobníkem volání

    Aktuální vlákno se také zobrazí v seznamu Vlákno na panelu nástrojů Ladění.

    Snímek obrazovky s aktuálním vláknem na panelu nástrojů Ladění

    Pomocí seznamu vláken můžete přepnout kontext ladicího programu na jiné vlákno. Tím se nezmění aktuálně běžící vlákno, pouze kontext ladícího prostředí.

    Případně můžete kontext ladicího programu přepnout tak, že dvakrát kliknete na metodu v zobrazení Vlákna nebo kliknete pravým tlačítkem myši na metodu v zobrazení Vlákna a vyberete Přepínač na Frame>[ID vlákna].

  6. Znovu stiskněte klávesu F5 a ladicí program se pozastaví v MorningWalk metodě pro druhé vlákno.

    Snímek obrazovky se zobrazením vláken za sekundou F5

    V závislosti na načasování provádění vlákna můžete v tomto okamžiku vidět buď samostatné, nebo seskupené zásobníky volání.

    Na předchozím obrázku jsou zásobníky volání pro dvě vlákna částečně seskupené. Identické segmenty zásobníků volání jsou seskupeny a šipky ukazují na segmenty, které jsou oddělené (tj. nejsou identické). Aktuální rámec zásobníku je označen modrým zvýrazněním.

  7. Znovu stiskněte klávesu F5 a dojde k dlouhému zpoždění a pohled Vlákna nezobrazuje žádné informace o zásobníku volání.

    Zpoždění je způsobeno zablokováním. V zobrazení Vlákna se nic nezobrazí, protože i když můžou být vlákna blokovaná, v ladicím programu nejste momentálně pozastaveni.

    Note

    V jazyce C++ se také zobrazí chyba ladění označující, že abort() se volala.

    Tip

    Tlačítko Přerušit vše je dobrým způsobem, jak získat informace o zásobníku volání, pokud dojde k zablokování nebo jsou v současnosti blokovaná všechna vlákna.

  8. V horní části integrovaného vývojového prostředí na panelu nástrojů Ladění vyberte tlačítko Přerušit vše (ikona pozastavení) nebo použijte kombinaci kláves Ctrl + Alt + Break.

    Snímek obrazovky se zobrazením vláken po výběru Přerušit vše.

    Horní část zásobníku volání v zobrazení Vlákna ukazuje, že FindBananas je zaseknutý. Instrukční ukazatel ve FindBananas je zvlněná zelená šipka, která označuje aktuální kontext ladicího programu, ale zároveň nám říká, že vlákna nejsou aktuálně spuštěna.

    Note

    V jazyce C++ nevidíte užitečné informace a ikony o vzájemném zablokování. Přesto však najdete vlněnou zelenou šipku v Jungle.FindBananas, naznačuje na místě zablokování.

    V editoru kódu najdeme ve funkci zvlněnou zelenou šipku lock . Tato dvě vlákna jsou ve lock funkci v FindBananas metodě blokována.

    Snímek obrazovky editoru kódu po výběru možnosti Přerušit vše

    V závislosti na pořadí provádění vlákna se zablokování zobrazí buď v příkazu lock(tree), nebo v příkazu lock(banana_bunch).

    Volání na lock zablokuje vlákna v metodě FindBananas. Jedno vlákno čeká, až druhé vlákno uvolní zámek na tree, ale toto druhé vlákno čeká, až bude uvolněn zámek na banana_bunch, než může uvolnit zámek na tree. Toto je příklad klasického vzájemného zablokování, ke kterému dochází, když na sebe čekají dvě vlákna.

    Pokud používáte Copilot, můžete také získat souhrny vláken generované AI, které vám pomůžou identifikovat potenciální zablokování.

    Snímek obrazovky se souhrnnými popisy vláken Copilot

Oprava vzorového kódu

Pokud chcete tento kód opravit, vždy získejte více zámků v konzistentním globálním pořadí ve všech vláknech. Zabráníte tak cirkulárním čekáním a eliminujete zablokování.

  1. Pokud chcete zablokování opravit, nahraďte kód MorningWalk následujícím kódem.

    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. Restartujte aplikaci.

Summary

Tento názorný postup ukázal ladicí okno Paralelní zásobníky. Toto okno použijte u skutečných projektů, které používají vícevláknový kód. Můžete prozkoumat paralelní kód napsaný v jazyce C++, C# nebo Visual Basic.