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


Отладка асинхронного приложения

В этом руководстве показано, как использовать представление "Задачи" окна Parallel Stacks для отладки асинхронного приложения C#. Это окно помогает понять и проверить поведение кода во время выполнения, использующего шаблон async/await, также называемый асинхронным шаблоном на основе задач (TAP).

** Для приложений, использующих библиотеку параллельных задач (TPL), но не асинхронный шаблон async/await, или для приложений C++ с использованием Библиотеки параллельных вычислений, вид потоки в окне Параллельные стеки является наиболее полезным инструментом для отладки. Дополнительные сведения см. в статье о просмотре потоков и задач в окне "Параллельные стеки".

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

  • Просмотр визуализаций стека вызовов для приложений, использующих шаблон async/await. В этих сценариях представление "Задачи" предоставляет более полное представление о состоянии приложения.

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

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

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

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

Пример включает использование паттерна "синхронизация поверх асинхронности", что может привести к истощению пула потоков.

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

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

Асинхронные стеки вызовов

Представление "Задачи" в Параллельных стеках предоставляет визуализацию для асинхронных стеков вызовов, чтобы увидеть, что происходит (или должно произойти) в приложении.

Ниже приведены несколько важных моментов, которые следует помнить при интерпретации данных в представлении "Задачи".

  • Стеки асинхронных вызовов — это логические или виртуальные стеки вызовов, а не физические стеки вызовов, представляющие стек. При работе с асинхронным кодом (например, с помощью await ключевого слова) отладчик предоставляет представление "асинхронных стеков вызовов" или "стеков виртуальных вызовов". Стеки асинхронных вызовов отличаются от стека вызовов на основе потоков или "физических стеков", так как асинхронные стеки вызовов не обязательно выполняются в настоящее время в любом физическом потоке. Вместо этого асинхронные стеки вызовов являются продолжением или "обещаниями" кода, который будет выполняться в будущем асинхронно. Стеки вызовов создаются с помощью продолжения.

  • Асинхронный код, запланированный, но не запущенный в данный момент, не отображается в стеке физических вызовов, но должен отображаться в асинхронном стеке вызовов в представлении "Задачи". Если вы блокируете потоки с помощью таких методов, как .Wait или .Result, вы можете увидеть код в стеке физических вызовов.

  • Стеки асинхронных виртуальных вызовов не всегда интуитивно понятны из-за ветвления, которое возникает при использовании вызовов методов, таких как .WaitAny или .WaitAll.

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

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

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

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

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

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

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

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

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

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

    Замечание

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

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

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

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

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

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

    using System.Diagnostics;
    
    namespace AsyncTasks_SyncOverAsync
    {
         class Jungle
         {
             public static async Task<int> FindBananas()
             {
                 await Task.Delay(1000);
                 Console.WriteLine("Got bananas.");
                 return 0;
             }
    
             static async Task Gorilla_Start()
             {
                 Debugger.Break();
                 Gorilla koko = new Gorilla();
                 int result = await Task.Run(koko.WakeUp);
             }
    
             static async Task Main(string[] args)
             {
                 List<Task> tasks = new List<Task>();
                 for (int i = 0; i < 2; i++)
                 {
                     Task task = Gorilla_Start();
                     tasks.Add(task);
    
                 }
                 await Task.WhenAll(tasks);
    
             }
         }
    
         class Gorilla
         {
    
             public async Task<int> WakeUp()
             {
                 int myResult = await MorningWalk();
    
                 return myResult;
             }
    
             public async Task<int> MorningWalk()
             {
                 int myResult = await Jungle.FindBananas();
                 GobbleUpBananas(myResult);
    
                 return myResult;
             }
    
             /// <summary>
             /// Calls a .Wait.
             /// </summary>
             public void GobbleUpBananas(int food)
             {
                 Console.WriteLine("Trying to gobble up food synchronously...");
    
                 Task mb = DoSomeMonkeyBusiness();
                 mb.Wait();
    
             }
    
             public async Task DoSomeMonkeyBusiness()
             {
                 Debugger.Break();
                 while (!System.Diagnostics.Debugger.IsAttached)
                 {
                     Thread.Sleep(100);
                 }
    
                 await Task.Delay(30000);
                 Console.WriteLine("Monkey business done");
             }
         }
    }
    

    После обновления файла кода сохраните изменения и создайте решение.

  4. В меню File (Файл) выберите команду Save All (Сохранить все).

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

Используйте представление задач окна параллельных стеков

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

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

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

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

    Снимок экрана вкладки

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

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

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

    Подсказка

    В окне стека вызовов можно отобразить такие сведения, как взаимоблокировка, используя описание Async cycle.

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

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

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

    В этом представлении показан более полный асинхронный стек вызовов после добавления более асинхронных методов в внутреннюю цепочку продолжения, которая возникает при использовании await и аналогичных методах. DoSomeMonkeyBusiness может или не присутствовать в верхней части асинхронного стека вызовов, так как это асинхронный метод, но еще не добавлен в цепочку продолжения. Мы рассмотрим, почему это так в следующих шагах.

    В этом представлении также показан значок заблокированного Jungle.Mainсостояния. Это информативно, но обычно не указывает на проблему. Заблокированная задача — это задача, которая заблокирована, так как она ожидает завершения другой задачи, сигнала события или освобождения блокировки.

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

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

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

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

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

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

    Снимок экрана: режим Задачи после второго нажатия F5.

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

    На предыдущем рисунке асинхронные стеки вызовов для двух задач разделены, так как они не идентичны.

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

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

    Подсказка

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

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

    Скриншот вида

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

    В левой части предыдущего снимка экрана свернутая зеленая стрелка указывает текущий контекст отладчика. Две задачи заблокированы на mb.Wait() в методе GobbleUpBananas.

    В окне стека вызовов также показано, что текущий поток заблокирован.

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

    Вызов Wait() блокирует потоки во время синхронного вызова GobbleUpBananas. Это пример антипаттерна синхронизации по-асинхронному, и если это произошло в потоке пользовательского интерфейса или при больших рабочих нагрузках обработки, оно обычно устраняется с помощью исправления кода await. Дополнительные сведения см. в разделе "Отладка нехватки пула потоков". Чтобы использовать средства профилирования для отладки нехватки пула потоков, ознакомьтесь с примером: изоляция проблемы с производительностью.

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

    Подсказка

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

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

  1. Замените метод GobbleUpBananas следующим кодом.

     public async Task GobbleUpBananas(int food) // Previously returned void.
     {
         Console.WriteLine("Trying to gobble up food...");
    
         //Task mb = DoSomeMonkeyBusiness();
         //mb.Wait();
         await DoSomeMonkeyBusiness();
     }
    
  2. В методе MorningWalk вызовите GobbleUpBananas с помощью await.

    await GobbleUpBananas(myResult);
    
  3. Нажмите кнопку Перезапустить (Ctrl + Shift + F5), а затем нажимайте клавишу F5 несколько раз, пока приложение не начнет подвисать.

  4. Нажмите клавишу BREAK ALL.

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

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

    Окно стека вызовов пусто, за исключением записи ExternalCode.

    Редактор кода не показывает нам ничего, кроме того, что он предоставляет сообщение, указывающее, что все потоки выполняют внешний код.

    Однако представление "Задачи" предоставляет полезную информацию. DoSomeMonkeyBusiness находится в верхней части асинхронного стека вызовов, как ожидалось. Это правильно указывает нам, где находится метод с длительным выполнением. Это полезно для изоляции асинхронных и ожиданий проблем, когда физический стек вызовов в окне "Стек вызовов" не предоставляет достаточно сведений.

Сводка

В этом пошаговом руководстве показано окно отладчика Parallel Stacks . Используйте это окно в приложениях, использующих шаблон async/await.