Pull to refresh

Руководство по отладке многопоточных приложений в Visual Studio 2010

Reading time 9 min
Views 19K
В этой статье я расскажу, как отлаживать многопоточные приложения в Visual Studio 2010, используя окна Parallel Tasks и Parallel Stacks. Эти окна помогут понять структуру выполнения многопоточных приложений и проверить правильность работы кода, который использует Task Parallel Library.

Мы научимся:
  • Как смотреть call stacks выполняемых потоков
  • Как посмотреть список заданий созданных в нашем приложении (System.Threading.Tasks.Task)
  • Как перемещаться в окнах отладки Parallel Tasks и Parallel Stacks
  • Узнаем интересные и полезные мелочи в отладки с vs2010


Осторожно, много картинок

Подготовка

Для тестов нам потребуется VS 2010. Изображения в этой статье получены с использованием процессора Intel Core i3

Код проекта

Код для языков VB и C++ можно найти на этой странице

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

class S
{
 static void Main()
 {
  pcount = Environment.ProcessorCount;
  Console.WriteLine("Proc count = " + pcount);
  ThreadPool.SetMinThreads(4, -1);
  ThreadPool.SetMaxThreads(4, -1);

  t1 = new Task(A, 1);
  t2 = new Task(A, 2);
  t3 = new Task(A, 3);
  t4 = new Task(A, 4);
  Console.WriteLine("Starting t1 " + t1.Id.ToString());
  t1.Start();
  Console.WriteLine("Starting t2 " + t2.Id.ToString());
  t2.Start();
  Console.WriteLine("Starting t3 " + t3.Id.ToString());
  t3.Start();
  Console.WriteLine("Starting t4 " + t4.Id.ToString());
  t4.Start();

  Console.ReadLine();
 }

 static void A(object o)
 {
  B(o);
 }
 static void B(object o)
 {
  C(o);
 }
 static void C(object o)
 {
  int temp = (int)o;

  Interlocked.Increment(ref aa);
  while (aa < 4)
  {
   ;
  }

  if (temp == 1)
  {
   // BP1 - all tasks in C
   Debugger.Break();
   waitFor1 = false;
  }
  else
  {
   while (waitFor1)
   {
    ;
   }
  }
  switch (temp)
  {
   case 1:
    D(o);
    break;
   case 2:
    F(o);
    break;
   case 3:
   case 4:
    I(o);
    break;
   default:
    Debug.Assert(false, "fool");
    break;
  }
 }
 static void D(object o)
 {
  E(o);
 }
 static void E(object o)
 {
  // break here at the same time as H and K
  while (bb < 2)
  {
   ;
  }
  //BP2 - 1 in E, 2 in H, 3 in J, 4 in K
  Debugger.Break();
  Interlocked.Increment(<font color="#0000ff">ref
bb);

  //after
  L(o);
 }
 static void F(object o)
 {
  G(o);
 }
 static void G(object o)
 {
  H(o);
 }
 static void H(object o)
 {
  // break here at the same time as E and K
  Interlocked.Increment(ref bb);
  Monitor.Enter(mylock);
  while (bb < 3)
  {
   ;
  }
  Monitor.Exit(mylock);

  //after
  L(o);
 }
 static void I(object o)
 {
  J(o);
 }
 static void J(object o)
 {
  int temp2 = (int)o;

  switch (temp2)
  {
   case 3:
    t4.Wait();
    break;
   case 4:
    K(o);
    break;
   default:
    Debug.Assert(false, "fool2");
    break;
  }
 }
 static void K(object o)
 {
  // break here at the same time as E and H
  Interlocked.Increment(ref bb);
  Monitor.Enter(mylock);
  while (bb < 3)
  {
   ;
  }
  Monitor.Exit(mylock);

  //after
  L(o);
 }
 static void L(object oo)
 {
  int temp3 = (int)oo;

  switch (temp3)
  {
   case 1:
    M(oo);
    break;
   case 2:
    N(oo);
    break;
   case 4:
    O(oo);
    break;
   default:
    Debug.Assert(false, "fool3");
    break;
  }
 }
 static void M(object o)
 {
  // breaks here at the same time as N and Q
  Interlocked.Increment(ref cc);
  while (cc < 3)
  {
   ;
  }
  //BP3 - 1 in M, 2 in N, 3 still in J, 4 in O, 5 in Q
  Debugger.Break();
  Interlocked.Increment(ref cc);
  while (true)
   Thread.Sleep(500); // for ever
 }
 static void N(object o)
 {
  // breaks here at the same time as M and Q
  Interlocked.Increment(ref cc);
  while (cc < 4)
  {
   ;
  }
  R(o);
 }
 static void O(object o)
 {
  Task t5 = Task.Factory.StartNew(P, TaskCreationOptions.AttachedToParent);
  t5.Wait();
  R(o);
 }
 static void P()
 {
  Console.WriteLine("t5 runs " + Task.CurrentId.ToString());
  Q();
 }
 static void Q()
 {
  // breaks here at the same time as N and M
  Interlocked.Increment(ref cc);
  while (cc < 4)
  {
   ;
  }
  // task 5 dies here freeing task 4 (its parent)
  Console.WriteLine("t5 dies " + Task.CurrentId.ToString());
  waitFor5 = false;
 }
 static void R(object o)
 {
  if ((int)o == 2)
  {
   //wait for task5 to die
   while (waitFor5) { ;}

   int i;
   //spin up all procs
   for (i = 0; i < pcount - 4; i++)
   {
    Task t = Task.Factory.StartNew(() => { while (true);});
    Console.WriteLine("Started task " + t.Id.ToString());
   }

   Task.Factory.StartNew(T, i + 1 + 5, TaskCreationOptions.AttachedToParent); //scheduled
   Task.Factory.StartNew(T, i + 2 + 5, TaskCreationOptions.AttachedToParent); //scheduled
   Task.Factory.StartNew(T, i + 3 + 5, TaskCreationOptions.AttachedToParent); //scheduled
   Task.Factory.StartNew(T, i + 4 + 5, TaskCreationOptions.AttachedToParent); //scheduled
   Task.Factory.StartNew(T, (i + 5 + 5).ToString(), TaskCreationOptions.AttachedToParent); //scheduled

   //BP4 - 1 in M, 2 in R, 3 in J, 4 in R, 5 died
   Debugger.Break();
  }
  else
  {
   Debug.Assert((int)o == 4);
   t3.Wait();
  }
 }
 static void T(object o)
 {
  Console.WriteLine("Scheduled run " + Task.CurrentId.ToString());
 }
 static Task t1, t2, t3, t4;
 static int aa = 0;
 static int bb = 0;
 static int cc = 0;
 static bool waitFor1 = true;
 static bool waitFor5 = true;
 static int pcount;
 static S mylock = new S();
}

* This source code was highlighted with Source Code Highlighter.


Parallel Stacks Window: Threads View (Потоки)


Шаг 1

Копируем код в студию в новый проект и запускаем в режиме отладки (F5). Программа скомпилируется, запустится и остановится в первой точке остановки.
В меню Debug→Windows нажимаем на Parallel Stacks. С помощью этого окна мы можем посмотреть несколько стеков вызовов параллельных потоков. На следующем рисунке показано состояние программы в первой точке остановки. Окно Call Stack включается там же, в меню Debug→Windows. Эти окна доступны только во время отладки программы. Во время написания кода их просто не видно.

image

На картинке 4 потока сгруппированы вместе, потому что их стек фреймы (stack frames) принадлежат одному контексту метода (method context), это значит, что это один и тот же метод (А, B, C). Чтобы посмотреть ID потока нужно навести на заголовок «4 Threads». Текущий поток будет выделен жирным. Желтая стрелка означает активный стек фрейм в текущем потоке. Чтобы получить дополнительную информацию нужно навести мышкой.

image

Чтобы убрать лишнюю информацию или включить (например название модуля, сдвиг, имена параметров и их типы и пр.) нужно щелкнуть правой кнопкой мышки по заголовку таблицы в окошке Call Stack (аналогично делается во всем окружении Windows).

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

Шаг 2

Продолжаем выполнение программы до второй точки остановки (F5). На следующем слайде видно состояние потоков во второй точке.

image

На первом шаге 4 потока пришли из методов A, B и C. Эта информация до сих пор доступна в окне Parallel Stacks, но теперь эти 4 потока получили развитие дальше. Один поток продолжился в D, затем в E. Другой в F, G и потом в H. Два остальных в I и J, а оттуда один из них направился в K а другой пошел своим путем в non-user External Code.

Можно переключиться на другой поток, для этого двойной щелчек на потоке. Я хочу посмотреть метод K. Для этого двойной клик по MyCalss.K

image

Parallel Stacks покажет информацию, а отладчик в студии покажет код этого места. Нажимаем Toggle Method View и наблюдаем картину истории (иерархии) методов до K.

image

Шаг 3

Продолжаем отладку до 3 прерывания.

image

Когда несколько потоков приходят в один и тот же метод, но метод не в начале стека вызовов – он появляется в разных рамках, как это произошло с методом L.
Двойной клик по методу L. Получаем такую картинку

image

Метод L выделен жирным так же в двух других рамках, так что можно видеть где он еще появится. Чтобы увидеть какие фреймы вызывают метод L переключаем режим отображения (Toggle Method View). Получаем следующее:

image

В контекстном меню есть такие пункты как «Hexadecimal Display» и «Show External Code». При включении последнего режима диаграмма получается больше чем предыдущая и содержит информацию о non-user code.

image

Шаг 4

Продолжаем выполнение программы до четвертого прерывания.

В этот раз диаграмма получится очень большой и на помощь приходит автоскролл, который сразу переводит на нужное место. The Bird's Eye View также помогает быстро ориентироваться в больших диаграммах. (маленькая кнопочка справа внизу). Авто зум и прочие радости помогают ориентироваться в действительно больших многопоточных приложениях.

image

Parallel Tasks Window и Tasks View в окне Parallel Stacks


Шаг 1

Завершаем работу программы (Shift + F5) или в меню отладки. Закрываем все лишние окошки с которыми мы экспериментировали в прошлом примере и открываем новые: Debug→Windows→Threads, Debug→Windows→Call Stack и лепим их к краям студии. Также открываем Debug→Windows→ Parallel Tasks. Вот что получилось в окне Parallel Tasks

image

Для каждого запущенного задания есть ID который возвращает значение одноименного свойства задания, местоположение задания (если навести мышь на Console, то появится целый стек вызовов) а также метод, который был принят как отправная точка задания (старт задания).

Шаг 2

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

image

Задание можно отметить флагом и следить за дальнейшим состоянием.

В окне Parallel Stack, которое мы использовали в предыдущем примере, есть переключатель просмотра с потоков на задания (слева вверху). Переключаем вид на Tasks

image

Шаг 3

image

Как видно из скриншота – новое задание 5 выполняется, а задачи 3 и 4 остановлены. Также можно изменить вид таблицы – правая кнопка мыши по заголовкам колонок. Если включить отображение предка задачи (Parent) то мы увидим, кто является предком задачи номер 5. Но для лучшей визуализации отношений можно включить специальный вид – ПКМ по колонке Parent→Parent Child View.

image

Окна Parallel Tasks и Parallel Stack – синхронизированы. Так что мы можем посмотреть какая задача в каком потоке выполняется. Например задача 4. Двойной клик по задаче 4 в окне Parallel Tasks, с этим кликом выполнится синхронизация с окном Parallel Stack и мы увидим такую картину

image

В окне Parallel Stack, в режиме задач можно перейти к поток. Для этого ПКМ на методе и Go To Thread. В нашем примере мы посмотрим что происходит с методом O.

image

Шаг 4

Продолжаем выполнение до следующей точки. Затем сортируем задачи по ID и видим следующее

image

В списке нет задачи 5, потому что она уже завершена. Задачи 3 и 4 ждут друг друга и зашли в тупик. Есть еще 5 новых задач от задачи 2, которые теперь запланированы на исполнение.

Вернемся в Parallel Stack. Подсказка в заголовке каждой таблички скажет, сколько задач заблокировано, сколько ожидают и сколько выполняется.

image

Задачи в списке задач можно сгруппировать. Например сгруппируем по статусу – ПКМ на колонке статус и Group by Status. Результат на скриншоте:

image

Еще несколько возможностей окошка Parallel Tasks: в контекстном меню можно заморозить задачи, можно заморозить основной поток задачи.

Заключение


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

Литература



Видео



Блог Daniel Moth



Спасибо за внимание и поменьше вам ошибок в многопоточных приложениях :)
Tags:
Hubs:
+31
Comments 9
Comments Comments 9

Articles