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

    В этой статье я расскажу, как отлаживать многопоточные приложения в 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



    Спасибо за внимание и поменьше вам ошибок в многопоточных приложениях :)
    Метки:
    • +31
    • 11,3k
    • 9
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 9
    • 0
      Спасибо. Не знал, что есть такой удобный отладчик.
      • +2
        Его не видно во время написания кода, поэтому в меню и незаметно. А когда режим отладки то мне тоже как то не доводилось заходить в это меню
        • 0
          Респект автору за очередную отличную статью.
          как раз возникла необходимость в такой отладке а знаний не было.
          еще раз благодарен :)
          • 0
            VS определенно лучший из продуктов MS. Офигенные возможности. Здоровская статья.
            • 0
              А прогресс сборки так и не научился показывать :(
              • 0
                И слава богу, что на всякую чушь не тратят свои девелоперские ресурсы.
                Кому-то шашечки а кому то ехать.

                Вот в Eclipse этот прогресс показывается, а простейшее действие implement method в CDT (среда для С++) занимает десятки секунд. То же относится к любому С++ рефакторингу.
                Поэтому я до сих пор софт даже для линукса и GCC пишу в VS под виндой.
            • 0
              жаль тут spoiler'a нет (возможности сворачивать часть текста), удобно было бы код туда помещать :)

              Спасибо, порадовала статья, особенно тот факт что на русском языке + очень много визуальной информации в виде изображений )))
              • 0
                Отличный материал, спасибо. Седня попробую поковырять сие ;)

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