В этой статье я расскажу, как отлаживать многопоточные приложения в Visual Studio 2010, используя окна Parallel Tasks и Parallel Stacks. Эти окна помогут понять структуру выполнения многопоточных приложений и проверить правильность работы кода, который использует Task Parallel Library.
Мы научимся:
Осторожно, много картинок
Для тестов нам потребуется VS 2010. Изображения в этой статье получены с использованием процессора Intel Core i3
Код для языков VB и C++ можно найти на этой странице
Копируем код в студию в новый проект и запускаем в режиме отладки (F5). Программа скомпилируется, запустится и остановится в первой точке остановки.
В меню Debug→Windows нажимаем на Parallel Stacks. С помощью этого окна мы можем посмотреть несколько стеков вызовов параллельных потоков. На следующем рисунке показано состояние программы в первой точке остановки. Окно Call Stack включается там же, в меню Debug→Windows. Эти окна доступны только во время отладки программы. Во время написания кода их просто не видно.
На картинке 4 потока сгруппированы вместе, потому что их стек фреймы (stack frames) принадлежат одному контексту метода (method context), это значит, что это один и тот же метод (А, B, C). Чтобы посмотреть ID потока нужно навести на заголовок «4 Threads». Текущий поток будет выделен жирным. Желтая стрелка означает активный стек фрейм в текущем потоке. Чтобы получить дополнительную информацию нужно навести мышкой.
Чтобы убрать лишнюю информацию или включить (например название модуля, сдвиг, имена параметров и их типы и пр.) нужно щелкнуть правой кнопкой мышки по заголовку таблицы в окошке Call Stack (аналогично делается во всем окружении Windows).
Голубая рамка вокруг означает, что текущий поток (который выделен жирным) является частью этих потоков.
Продолжаем выполнение программы до второй точки остановки (F5). На следующем слайде видно состояние потоков во второй точке.
На первом шаге 4 потока пришли из методов A, B и C. Эта информация до сих пор доступна в окне Parallel Stacks, но теперь эти 4 потока получили развитие дальше. Один поток продолжился в D, затем в E. Другой в F, G и потом в H. Два остальных в I и J, а оттуда один из них направился в K а другой пошел своим путем в non-user External Code.
Можно переключиться на другой поток, для этого двойной щелчек на потоке. Я хочу посмотреть метод K. Для этого двойной клик по MyCalss.K
Parallel Stacks покажет информацию, а отладчик в студии покажет код этого места. Нажимаем Toggle Method View и наблюдаем картину истории (иерархии) методов до K.
Продолжаем отладку до 3 прерывания.
Когда несколько потоков приходят в один и тот же метод, но метод не в начале стека вызовов – он появляется в разных рамках, как это произошло с методом L.
Двойной клик по методу L. Получаем такую картинку
Метод L выделен жирным так же в двух других рамках, так что можно видеть где он еще появится. Чтобы увидеть какие фреймы вызывают метод L переключаем режим отображения (Toggle Method View). Получаем следующее:
В контекстном меню есть такие пункты как «Hexadecimal Display» и «Show External Code». При включении последнего режима диаграмма получается больше чем предыдущая и содержит информацию о non-user code.
Продолжаем выполнение программы до четвертого прерывания.
В этот раз диаграмма получится очень большой и на помощь приходит автоскролл, который сразу переводит на нужное место. The Bird's Eye View также помогает быстро ориентироваться в больших диаграммах. (маленькая кнопочка справа внизу). Авто зум и прочие радости помогают ориентироваться в действительно больших многопоточных приложениях.
Завершаем работу программы (Shift + F5) или в меню отладки. Закрываем все лишние окошки с которыми мы экспериментировали в прошлом примере и открываем новые: Debug→Windows→Threads, Debug→Windows→Call Stack и лепим их к краям студии. Также открываем Debug→Windows→ Parallel Tasks. Вот что получилось в окне Parallel Tasks
Для каждого запущенного задания есть ID который возвращает значение одноименного свойства задания, местоположение задания (если навести мышь на Console, то появится целый стек вызовов) а также метод, который был принят как отправная точка задания (старт задания).
В предыдущий раз все задания были отмечены как выполняемые, сейчас 2 задания заблокированы по разным причинам. Чтобы узнать причину нужно навести мышь на задание.
Задание можно отметить флагом и следить за дальнейшим состоянием.
В окне Parallel Stack, которое мы использовали в предыдущем примере, есть переключатель просмотра с потоков на задания (слева вверху). Переключаем вид на Tasks
Как видно из скриншота – новое задание 5 выполняется, а задачи 3 и 4 остановлены. Также можно изменить вид таблицы – правая кнопка мыши по заголовкам колонок. Если включить отображение предка задачи (Parent) то мы увидим, кто является предком задачи номер 5. Но для лучшей визуализации отношений можно включить специальный вид – ПКМ по колонке Parent→Parent Child View.
Окна Parallel Tasks и Parallel Stack – синхронизированы. Так что мы можем посмотреть какая задача в каком потоке выполняется. Например задача 4. Двойной клик по задаче 4 в окне Parallel Tasks, с этим кликом выполнится синхронизация с окном Parallel Stack и мы увидим такую картину
В окне Parallel Stack, в режиме задач можно перейти к поток. Для этого ПКМ на методе и Go To Thread. В нашем примере мы посмотрим что происходит с методом O.
Продолжаем выполнение до следующей точки. Затем сортируем задачи по ID и видим следующее
В списке нет задачи 5, потому что она уже завершена. Задачи 3 и 4 ждут друг друга и зашли в тупик. Есть еще 5 новых задач от задачи 2, которые теперь запланированы на исполнение.
Вернемся в Parallel Stack. Подсказка в заголовке каждой таблички скажет, сколько задач заблокировано, сколько ожидают и сколько выполняется.
Задачи в списке задач можно сгруппировать. Например сгруппируем по статусу – ПКМ на колонке статус и Group by Status. Результат на скриншоте:
Еще несколько возможностей окошка Parallel Tasks: в контекстном меню можно заморозить задачи, можно заморозить основной поток задачи.
С помощью этих двух мощных инструментов можно отлаживать большие и сложные, многопоточные приложения. Смотреть за результатом и порядком выполнения задач. Планировать правильный порядок выполнения задач. Строить программы с минимальным количеством ошибок, связанных с непониманием работы многопоточных программ.
Спасибо за внимание и поменьше вам ошибок в многопоточных приложениях :)
Мы научимся:
- Как смотреть 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. Эти окна доступны только во время отладки программы. Во время написания кода их просто не видно.
На картинке 4 потока сгруппированы вместе, потому что их стек фреймы (stack frames) принадлежат одному контексту метода (method context), это значит, что это один и тот же метод (А, B, C). Чтобы посмотреть ID потока нужно навести на заголовок «4 Threads». Текущий поток будет выделен жирным. Желтая стрелка означает активный стек фрейм в текущем потоке. Чтобы получить дополнительную информацию нужно навести мышкой.
Чтобы убрать лишнюю информацию или включить (например название модуля, сдвиг, имена параметров и их типы и пр.) нужно щелкнуть правой кнопкой мышки по заголовку таблицы в окошке Call Stack (аналогично делается во всем окружении Windows).
Голубая рамка вокруг означает, что текущий поток (который выделен жирным) является частью этих потоков.
Шаг 2
Продолжаем выполнение программы до второй точки остановки (F5). На следующем слайде видно состояние потоков во второй точке.
На первом шаге 4 потока пришли из методов A, B и C. Эта информация до сих пор доступна в окне Parallel Stacks, но теперь эти 4 потока получили развитие дальше. Один поток продолжился в D, затем в E. Другой в F, G и потом в H. Два остальных в I и J, а оттуда один из них направился в K а другой пошел своим путем в non-user External Code.
Можно переключиться на другой поток, для этого двойной щелчек на потоке. Я хочу посмотреть метод K. Для этого двойной клик по MyCalss.K
Parallel Stacks покажет информацию, а отладчик в студии покажет код этого места. Нажимаем Toggle Method View и наблюдаем картину истории (иерархии) методов до K.
Шаг 3
Продолжаем отладку до 3 прерывания.
Когда несколько потоков приходят в один и тот же метод, но метод не в начале стека вызовов – он появляется в разных рамках, как это произошло с методом L.
Двойной клик по методу L. Получаем такую картинку
Метод L выделен жирным так же в двух других рамках, так что можно видеть где он еще появится. Чтобы увидеть какие фреймы вызывают метод L переключаем режим отображения (Toggle Method View). Получаем следующее:
В контекстном меню есть такие пункты как «Hexadecimal Display» и «Show External Code». При включении последнего режима диаграмма получается больше чем предыдущая и содержит информацию о non-user code.
Шаг 4
Продолжаем выполнение программы до четвертого прерывания.
В этот раз диаграмма получится очень большой и на помощь приходит автоскролл, который сразу переводит на нужное место. The Bird's Eye View также помогает быстро ориентироваться в больших диаграммах. (маленькая кнопочка справа внизу). Авто зум и прочие радости помогают ориентироваться в действительно больших многопоточных приложениях.
Parallel Tasks Window и Tasks View в окне Parallel Stacks
Шаг 1
Завершаем работу программы (Shift + F5) или в меню отладки. Закрываем все лишние окошки с которыми мы экспериментировали в прошлом примере и открываем новые: Debug→Windows→Threads, Debug→Windows→Call Stack и лепим их к краям студии. Также открываем Debug→Windows→ Parallel Tasks. Вот что получилось в окне Parallel Tasks
Для каждого запущенного задания есть ID который возвращает значение одноименного свойства задания, местоположение задания (если навести мышь на Console, то появится целый стек вызовов) а также метод, который был принят как отправная точка задания (старт задания).
Шаг 2
В предыдущий раз все задания были отмечены как выполняемые, сейчас 2 задания заблокированы по разным причинам. Чтобы узнать причину нужно навести мышь на задание.
Задание можно отметить флагом и следить за дальнейшим состоянием.
В окне Parallel Stack, которое мы использовали в предыдущем примере, есть переключатель просмотра с потоков на задания (слева вверху). Переключаем вид на Tasks
Шаг 3
Как видно из скриншота – новое задание 5 выполняется, а задачи 3 и 4 остановлены. Также можно изменить вид таблицы – правая кнопка мыши по заголовкам колонок. Если включить отображение предка задачи (Parent) то мы увидим, кто является предком задачи номер 5. Но для лучшей визуализации отношений можно включить специальный вид – ПКМ по колонке Parent→Parent Child View.
Окна Parallel Tasks и Parallel Stack – синхронизированы. Так что мы можем посмотреть какая задача в каком потоке выполняется. Например задача 4. Двойной клик по задаче 4 в окне Parallel Tasks, с этим кликом выполнится синхронизация с окном Parallel Stack и мы увидим такую картину
В окне Parallel Stack, в режиме задач можно перейти к поток. Для этого ПКМ на методе и Go To Thread. В нашем примере мы посмотрим что происходит с методом O.
Шаг 4
Продолжаем выполнение до следующей точки. Затем сортируем задачи по ID и видим следующее
В списке нет задачи 5, потому что она уже завершена. Задачи 3 и 4 ждут друг друга и зашли в тупик. Есть еще 5 новых задач от задачи 2, которые теперь запланированы на исполнение.
Вернемся в Parallel Stack. Подсказка в заголовке каждой таблички скажет, сколько задач заблокировано, сколько ожидают и сколько выполняется.
Задачи в списке задач можно сгруппировать. Например сгруппируем по статусу – ПКМ на колонке статус и Group by Status. Результат на скриншоте:
Еще несколько возможностей окошка Parallel Tasks: в контекстном меню можно заморозить задачи, можно заморозить основной поток задачи.
Заключение
С помощью этих двух мощных инструментов можно отлаживать большие и сложные, многопоточные приложения. Смотреть за результатом и порядком выполнения задач. Планировать правильный порядок выполнения задач. Строить программы с минимальным количеством ошибок, связанных с непониманием работы многопоточных программ.
Литература
- Using the Parallel Stacks Window
- Using the Parallel Tasks Window
- Walkthrough: Debugging a Parallel Application (примеры кода тут)
- Task Parallel Library
Видео
- Parallel Stacks – new Visual Studio 2010 debugger window
- Parallel Tasks – new Visual Studio 2010 debugger window
- VS2010 Parallel Computing Features Tour
- Parallel Debugging in Visual Studio 2010
Блог Daniel Moth
Спасибо за внимание и поменьше вам ошибок в многопоточных приложениях :)