Поделиться через


Отладка взаимоблокировки с помощью представления "Потоки"

В этом руководстве показано, как использовать представление "Потоки " окон Параллельных стеков для отладки многопоточного приложения C#. Это окно помогает понять и проверить поведение многопоточного кода во время выполнения.

Представление Threads также поддерживается для C++ и Visual Basic, поэтому те же принципы, описанные в этой статье для C# также применяются к C++ и Visual Basic.

Представление "Потоки" помогает вам:

  • Просмотр визуализаций стека вызовов для нескольких потоков, что обеспечивает более полную картину состояния приложения, чем окно стека вызовов, которое просто показывает стек вызовов для текущего потока.

  • Помогите определить такие проблемы, как блокированные или взаимоблокированные потоки.

Пример на языке C#

Пример кода в этом пошаговом руководстве предназначен для приложения, которое имитирует день в жизни гориллы. Цель упражнения — понять, как использовать представление "Потоки" окна Параллельных стеков для отладки многопоточного приложения.

Пример включает пример взаимоблокировки, который возникает, когда два потока ожидают друг друга.

Чтобы сделать стек вызовов интуитивно понятным, пример приложения выполняет следующие последовательные действия.

  1. Создает объект, представляющий гориллу.
  2. Горилла просыпается.
  3. Горилла идет на утренней прогулке.
  4. Горилла находит бананы в джунглях.
  5. Горилла ест.
  6. Горилла занимается обезьяньим бизнесом.

Многопоточные стеки вызовов

Идентичные разделы стека вызовов группируются вместе, чтобы упростить визуализацию для сложных приложений.

В следующей концептуальной анимации показано, как группирование применяется к стекам вызовов. Группируются только идентичные сегменты стека вызовов.

Иллюстрация группировки стеков вызовов.

Создание примера проекта

Чтобы создать проект, выполните следующие действия.

  1. Откройте Visual Studio и создайте проект.

    Если окно запуска не открыто, выберите Файл>Окно запуска.

    В окне "Пуск" выберите новый проект.

    В окне Создание нового проекта введите консоль в поле поиска. Затем выберите C# в списке языков, а затем выберите Windows из списка платформ.

    После применения фильтров языка и платформы выберите консольное приложение для .NET и нажмите кнопку "Далее".

    Note

    Если вы не видите правильный шаблон, перейдите к разделу "Сервис>получения инструментов и компонентов", который открывает установщик Visual Studio. Выберите компонент разработки настольных приложений .NET, а затем выберите Изменить.

    В окне "Настройка нового проекта" введите имя или используйте имя по умолчанию в поле "Имя проекта ". Теперь щелкните Далее.

    Для .NET выберите рекомендуемую целевую платформу или .NET 8, а затем нажмите кнопку "Создать".

    Появится новый проект консоли. После создания проекта появится исходный файл.

  2. Откройте файл кода .cs в проекте. Удалите его содержимое, чтобы создать пустой файл кода.

  3. Вставьте следующий код для выбранного языка в пустой файл кода.

     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. В меню File (Файл) выберите команду Save All (Сохранить все).

  5. В меню Сборка выберите команду Собрать решение.

Использование представления "Потоки" окна Параллельных стеков

Чтобы начать отладку, выполните следующие действия.

  1. В меню Отладка выберите Начать отладку (или F5) и дождитесь, пока не будет достигнута первая Debugger.Break() точка останова.

  2. Нажмите клавишу F5 один раз, и отладчик снова приостанавливается в той же Debugger.Break() строке.

    Приостановка происходит при втором вызове Gorilla_Start, который осуществляется в параллельном потоке.

    Tip

    Отладчик останавливает выполнение кода по каждому потоку. Например, это означает, что если вы нажмете F5, чтобы продолжить выполнение, и приложение достигнет следующей точки останова, оно может перейти в код на другом потоке. Если вам нужно управлять этим поведением для отладки, можно добавить дополнительные точки останова, условные точки останова или использовать команду "Отключить все". Дополнительные сведения об использовании условных точек останова см. в разделе "Следуйте одному потоку с условными точками останова".

  3. Выберите "Отладка > параллельных стеков Windows > " , чтобы открыть окно параллельных стеков, а затем выберите потоки в раскрывающемся списке "Представление ".

    Снимок экрана: представление

    В представлении Threads стековый кадр и путь вызова текущего потока выделены синим цветом. Текущее расположение потока отображается желтой стрелкой.

    Обратите внимание, что метка для стека вызовов Gorilla_Start2 Threads. После последнего нажатия клавиши F5 вы начали еще один поток. Для упрощения в сложных приложениях идентичные стеки вызовов группируются в одно визуальное представление. Это упрощает потенциально сложную информацию, особенно в сценариях с множеством потоков.

    Во время отладки можно переключить, отображается ли внешний код. Чтобы переключить функцию, выберите или снимите флажок "Показать внешний код". Если вы показываете внешний код, вы по-прежнему можете использовать это пошаговое руководство, но результаты могут отличаться от иллюстраций.

  4. Снова нажмите клавишу F5 , а отладчик приостанавливается в строке Debugger.Break() метода MorningWalk .

    В окне Parallel Stacks отображается расположение текущего выполняемого потока в методе MorningWalk .

    Снимок экрана: представление «Потоки» после F5.

  5. Наведите указатель мыши на MorningWalk метод, чтобы получить сведения о двух потоках, представленных стеком сгруппированных вызовов.

    Снимок экрана: потоки, связанные с стеком вызовов.

    Текущий поток также отображается в списке потоков на панели инструментов отладки.

    Снимок экрана текущего потока на панели инструментов отладки.

    Список потоков можно использовать для переключения контекста отладчика на другой поток. Это не изменяет текущий исполняемый поток, а только контекст отладчика.

    Кроме того, можно переключить контекст отладчика, дважды щелкнув метод в представлении "Потоки", или щелкнув правой кнопкой мыши метод в представлении "Потоки" и выбрав параметр Switch to Frame>[thread ID].

  6. Нажмите клавишу F5 еще раз, а отладчик приостанавливается в методе MorningWalk для второго потока.

    Снимок экрана: представление

    В зависимости от времени выполнения потока на этом этапе отображаются отдельные или сгруппированные стеки вызовов.

    На предыдущем рисунке стеки вызовов для двух потоков частично группируются. Идентичные сегменты стека вызовов группируются, а строки со стрелками указывают на сегменты, разделенные (т. е. не идентичны). Текущий кадр стека обозначается синим выделением.

  7. Нажмите клавишу F5 еще раз, и вы увидите длинную задержку, и представление "Потоки" не отображает сведения о стеке вызовов.

    Задержка вызвана взаимоблокировкой. В представлении "Потоки" ничего не отображается, потому что, хотя потоки могут быть заблокированы, вы в настоящий момент не приостановлены в отладчике.

    Tip

    Кнопка "Разорвать все " — это хороший способ получить сведения о стеке вызовов, если взаимоблокировка возникает или все потоки в настоящее время блокируются.

  8. В верхней части интегрированной среды разработки на панели инструментов отладки нажмите кнопку "Разорвать все " (значок приостановки) или нажмите клавиши CTRL+ALT+BREAK.

    Снимок экрана: представление

    В верхней части стека вызовов в представлении "Потоки" показано, что FindBananas находится во взаимоблокировке. Указатель исполнения в FindBananas — это закрученная зеленая стрелка, указывающая текущий контекст отладчика; она также сообщает нам, что потоки не выполняются в данный момент.

    В редакторе кода мы находим закрученную зеленую стрелку в lock функции. Два потока блокируются на функции lock в методе FindBananas.

    Снимок экрана: редактор кода после нажатия кнопки

    В зависимости от порядка выполнения потока, взаимоблокировка возникает либо в инструкции lock(tree), либо в инструкции lock(banana_bunch).

    Вызов lock блокирует потоки в методе FindBananas. Один поток ожидает освобождения блокировки tree другим потоком, но другой поток ожидает освобождения блокировки banana_bunch , прежде чем он сможет освободить блокировку tree. Это пример классической взаимоблокировки, которая возникает, когда два потока ожидают друг друга.

    Если вы используете Copilot, вы также можете получить суммарные описания потоков, созданные ИИ, чтобы помочь определить потенциальные взаимоблокировки.

    Снимок экрана: краткое описание потока Copilot.

Исправлен пример кода

Чтобы исправить этот код, всегда захватывайте несколько блокировок в согласованном глобальном порядке во всех потоках. Это предотвращает циклические ожидания и устраняет взаимоблокировки.

  1. Чтобы исправить взаимоблокировку, замените код в 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. Перезапустите приложение.

Summary

В этом пошаговом руководстве показано окно отладчика Parallel Stacks . Используйте это окно в реальных проектах, использующих многопоточный код. Можно изучить параллельный код, написанный на C++, C#или Visual Basic.