연습: 병렬 응용 프로그램 디버깅
이 연습에서는 병렬 작업 및 병렬 스택 창을 사용하여 병렬 응용 프로그램을 디버깅하는 방법을 보여 줍니다. 이 두 창은 작업 병렬 라이브러리 또는 동시성 런타임을 사용하는 코드의 런타임 동작을 이해하고 확인하는 데 도움이 됩니다. 이 연습에서는 기본 제공 중단점이 있는 샘플 코드를 제공합니다. 또한 코드가 중단된 후 병렬 작업 및 병렬 스택 창을 사용하여 코드를 검사하는 방법을 보여 줍니다.
이 연습에서는 다음 작업 방법을 배웁니다.
한 뷰에서 모든 스레드의 호출 스택을 보는 방법
응용 프로그램에서 만들어지는 System.Threading.Tasks.Task 인스턴스의 목록을 보는 방법
스레드 대신 작업의 실제 호출 스택을 보는 방법
병렬 작업 및 병렬 스택 창에서 코드를 탐색하는 방법
창에서 그룹화, 확대/축소 및 기타 관련 기능을 통한 크기 조정을 처리하는 방법
사전 요구 사항
컴퓨터에 Visual Studio 2010이 설치되어 있어야 합니다.
이 연습에서는 내 코드만이 사용된다고 가정합니다. 도구 메뉴에서 옵션을 클릭하고 디버깅 노드를 확장한 다음 일반을 선택하고 **내 코드만 사용(관리 전용)**을 선택합니다. 이 기능을 설정하지 않아도 연습을 사용할 수 있지만 결과가 그림과 다를 수 있습니다.
C# 샘플
C# 샘플을 사용하는 경우 이 연습에서는 외부 코드가 숨겨져 있다고 가정합니다. 외부 코드의 표시 여부를 전환하려면 호출 스택 창의 이름 표 머리글을 마우스 오른쪽 단추로 클릭하고 외부 코드 표시를 선택하거나 지웁니다. 이 기능을 설정하지 않아도 연습을 사용할 수 있지만 결과가 그림과 다를 수 있습니다.
C++ 샘플
C++ 샘플을 사용하는 경우 이 항목의 외부 코드에 대한 참조를 무시해도 됩니다. 외부 코드는 C# 샘플에만 적용됩니다.
그림
이 항목의 그림은 C# 샘플을 실행하는 쿼드 코어 컴퓨터에서 기록되었습니다. 다른 구성을 사용하여 이 연습을 수행할 수도 있지만 그림이 컴퓨터에 표시되는 것과 다를 수 있습니다.
샘플 프로젝트 만들기
이 연습의 샘플 코드는 아무 작업도 수행하지 않는 응용 프로그램의 코드입니다. 이 코드의 목표는 단지 도구 창을 사용하여 병렬 응용 프로그램을 디버깅하는 방법을 이해하는 것입니다.
샘플 프로젝트를 만들려면
Visual Studio의 파일 메뉴에서 새로 만들기를 가리킨 다음 프로젝트를 클릭합니다.
설치된 템플릿 창에서 Visual C#, Visual Basic 또는 Visual C++를 선택합니다. 관리되는 언어의 경우 .NET Framework 4이 프레임워크 상자에 표시되는지 확인합니다.
콘솔 응용 프로그램을 선택하고 확인을 클릭합니다. 기본값인 Debug 구성을 유지합니다.
프로젝트에서 .cpp, .cs 또는 .vb 코드 파일을 엽니다. 내용을 삭제하여 빈 코드 파일을 만듭니다.
선택한 언어의 다음 코드를 빈 코드 파일에 붙여 넣습니다.
Imports System
Imports System.Threading
Imports System.Threading.Tasks
Imports System.Diagnostics
Module S
Sub Main()
pcount = Environment.ProcessorCount
Console.WriteLine("Proc count = " + pcount.ToString())
ThreadPool.SetMinThreads(4, -1)
ThreadPool.SetMaxThreads(4, -1)
t1 = New Task(AddressOf A, 1)
t2 = New Task(AddressOf A, 2)
t3 = New Task(AddressOf A, 3)
t4 = New Task(AddressOf 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()
End Sub
Sub A(ByVal o As Object)
B(o)
End Sub
Sub B(ByVal o As Object)
C(o)
End Sub
Sub C(ByVal o As Object)
Dim temp As Integer = o
Interlocked.Increment(aa)
While (aa < 4)
End While
If (temp = 1) Then
' BP1 - all tasks in C
Debugger.Break()
waitFor1 = False
Else
While (waitFor1)
End While
End If
Select Case temp
Case 1
D(o)
Case 2
F(o)
Case 3, 4
I(o)
Case Else
Debug.Assert(False, "fool")
End Select
End Sub
Sub D(ByVal o As Object)
E(o)
End Sub
Sub E(ByVal o As Object)
' break here at the same time as H and K
While (bb < 2)
End While
'BP2 - 1 in E, 2 in H, 3 in J, 4 in K
Debugger.Break()
Interlocked.Increment(bb)
'after
L(o)
End Sub
Sub F(ByVal o As Object)
G(o)
End Sub
Sub G(ByVal o As Object)
H(o)
End Sub
Sub H(ByVal o As Object)
' break here at the same time as E and K
Interlocked.Increment(bb)
Monitor.Enter(mylock)
While (bb < 3)
End While
Monitor.Exit(mylock)
'after
L(o)
End Sub
Sub I(ByVal o As Object)
J(o)
End Sub
Sub J(ByVal o As Object)
Dim temp2 As Integer = o
Select Case temp2
Case 3
t4.Wait()
Case 4
K(o)
Case Else
Debug.Assert(False, "fool2")
End Select
End Sub
Sub K(ByVal o As Object)
' break here at the same time as E and H
Interlocked.Increment(bb)
Monitor.Enter(mylock)
While (bb < 3)
End While
Monitor.Exit(mylock)
'after
L(o)
End Sub
Sub L(ByVal oo As Object)
Dim temp3 As Integer = oo
Select Case temp3
Case 1
M(oo)
Case 2
N(oo)
Case 4
O(oo)
Case Else
Debug.Assert(False, "fool3")
End Select
End Sub
Sub M(ByVal o As Object)
' breaks here at the same time as N and Q
Interlocked.Increment(cc)
While (cc < 3)
End While
'BP3 - 1 in M, 2 in N, 3 still in J, 4 in O, 5 in Q
Debugger.Break()
Interlocked.Increment(cc)
While (True)
Thread.Sleep(500) ' for ever
End While
End Sub
Sub N(ByVal o As Object)
' breaks here at the same time as M and Q
Interlocked.Increment(cc)
While (cc < 4)
End While
R(o)
End Sub
Sub O(ByVal o As Object)
Dim t5 As Task = Task.Factory.StartNew(AddressOf P, TaskCreationOptions.AttachedToParent)
t5.Wait()
R(o)
End Sub
Sub P()
Console.WriteLine("t5 runs " + Task.CurrentId.ToString())
Q()
End Sub
Sub Q()
' breaks here at the same time as N and M
Interlocked.Increment(cc)
While (cc < 4)
End While
' task 5 dies here freeing task 4 (its parent)
Console.WriteLine("t5 dies " + Task.CurrentId.ToString())
waitFor5 = False
End Sub
Sub R(ByVal o As Object)
If (o = 2) Then
' wait for task5 to die
While waitFor5
End While
'//spin up all procs
Dim i As Integer
For i = 0 To pcount - 4 - 1
Dim t As Task = Task.Factory.StartNew(Sub()
While True
End While
End Sub)
Console.WriteLine("Started task " + t.Id.ToString())
Next
Task.Factory.StartNew(AddressOf T, i + 1 + 5, TaskCreationOptions.AttachedToParent) ' //scheduled
Task.Factory.StartNew(AddressOf T, i + 2 + 5, TaskCreationOptions.AttachedToParent) ' //scheduled
Task.Factory.StartNew(AddressOf T, i + 3 + 5, TaskCreationOptions.AttachedToParent) ' //scheduled
Task.Factory.StartNew(AddressOf T, i + 4 + 5, TaskCreationOptions.AttachedToParent) ' //scheduled
Task.Factory.StartNew(AddressOf 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(o = 4)
t3.Wait()
End If
End Sub
Sub T(ByVal o As Object)
Console.WriteLine("Scheduled run " + Task.CurrentId.ToString())
End Sub
Private t1, t2, t3, t4 As Task
Private aa As Integer = 0
Private bb As Integer = 0
Private cc As Integer = 0
Private waitFor1 As Boolean = True
Private waitFor5 As Boolean = True
Private pcount As Integer
Private mylock As New S2()
End Module
Public Class S2
End Class
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(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();
}
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <ppl.h>
#include <agents.h>
#include <stdio.h>
#include <concrtrm.h>
#include <vector>
CRITICAL_SECTION cs;
using namespace ::std;
using namespace ::std::tr1;
using namespace ::Concurrency;
task_group task4;
task_group task3;
task_group task2;
volatile long aa = 0;
volatile long bb = 0;
volatile long cc = 0;
static bool waitFor1 = true;
static bool waitFor5 = true;
#pragma optimize("", off)
void Spin()
{
for(int i=0;i<50*50000;++i);
}
#pragma optimize("",on)
template<class Func>
class RunFunc
{
Func& m_Func;
int m_o;
public:
RunFunc(Func func,int o):m_Func(func),m_o(o){
};
void operator()()const{
m_Func(m_o);
};
};
void T(int o)
{
cout << "Scheduled run \n";
};
void R(int o)
{
if (o == 2)
{
while (waitFor5) { ;}
Spin();
//use up all processors but 4 by scheduling 4 non-terminating tasks.
int numProcsToBurn = GetProcessorCount() - 4;
int i;
vector<call<int>*> tasks;
for (i = 0; i < numProcsToBurn; i++)
{
tasks.push_back(new call<int>([](int i){while(true)Spin();}));
asend(tasks[i],1);
cout << "Started task \n";
}
task_handle<RunFunc<decltype(T)>> t6(RunFunc<decltype(T)>(T,i + 1 + 5));
task_handle<RunFunc<decltype(T)>> t7(RunFunc<decltype(T)>(T,i + 2 + 5));
task_handle<RunFunc<decltype(T)>> t8(RunFunc<decltype(T)>(T,i + 3 + 5));
task_handle<RunFunc<decltype(T)>> t9(RunFunc<decltype(T)>(T,i + 4 + 5));
task_handle<RunFunc<decltype(T)>> t10(RunFunc<decltype(T)>(T,i + 5 + 5));
task2.run(t6);
task2.run(t7);
task2.run(t8);
task2.run(t9);
task2.run(t10);
//BP4 - 1 in M, 2 in R, 3 in J, 4 in R, 5 died
DebugBreak();
}
else
{
if (o!=4)
throw;
task3.wait();
}
};
void Q()
{
// breaks here at the same time as N and M
InterlockedIncrement(& cc);
while (cc < 4)
{
;
}
// task 5 dies here freeing task 4 (its parent)
cout << "t5 dies\n";
waitFor5 = false;
};
void P()
{
cout << "t5 runs\n";
Q();
};
void O(int o)
{
task_group t5;
t5.run(&P);
t5.wait();
R(o);
};
void N(int o)
{
// breaks here at the same time as M and Q
InterlockedIncrement(&cc);
while (cc < 4)
{
;
}
R(o);
};
void M(int o)
{
// breaks here at the same time as N and Q
InterlockedIncrement(&cc);
while (cc < 3)
{
;
}
//BP3 - 1 in M, 2 in N, 3 still in J, 4 in O, 5 in Q
DebugBreak();
InterlockedIncrement(&cc);
while (true)
Sleep(500); // for ever
};
void L(int oo)
{
int temp3 = oo;
switch (temp3)
{
case 1:
M(oo);
break;
case 2:
N(oo);
break;
case 4:
O(oo);
break;
default:
throw; //fool3
break;
}
}
void K(int o)
{
// break here at the same time as E and H
InterlockedIncrement(&bb);
EnterCriticalSection(&cs);
while (bb < 3)
{
;
}
LeaveCriticalSection(&cs);
Spin();
//after
L(o);
}
void J(int o)
{
int temp2 = o;
switch (temp2)
{
case 3:
task4.wait();
break;
case 4:
K(o);
break;
default:
throw; //fool2
break;
}
}
static void I(int o)
{
J(o);
}
static void H(int o)
{
// break here at the same time as E and K
InterlockedIncrement(&bb);
EnterCriticalSection(&cs);
while (bb < 3)
{
;
}
LeaveCriticalSection(&cs);
Spin();
//after
L(o);
}
static void G(int o)
{
H(o);
}
static void F(int o)
{
G(o);
}
static void E(int 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
Spin(); // for native case only
DebugBreak();
InterlockedIncrement(&bb);
//after
L(o);
}
static void D(int o)
{
E(o);
}
static void C(int o)
{
int temp = o;
InterlockedIncrement(&aa);
while (aa < 4)
{
;
}
if (temp == 1)
{
// BP1 - all tasks in C
DebugBreak();
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:
throw; //fool
break;
}
}
static void B(int o)
{
C(o);
}
void A(int o)
{
B(o);
}
int main()
{
InitializeCriticalSection(&cs);
task_group tasks;
task_handle<RunFunc<decltype(A)>> t1(RunFunc<decltype(A)>(A,1));
tasks.run(t1);
task_handle<RunFunc<decltype(A)>> t2(RunFunc<decltype(A)>(A,2));
task2.run(t2);
task_handle<RunFunc<decltype(A)>> t3(RunFunc<decltype(A)>(A,3));
task3.run(t3);
task_handle<RunFunc<decltype(A)>> t4(RunFunc<decltype(A)>(A,4));
task4.run(t4);
getchar();
return 1;
}
파일 메뉴에서 모두 저장을 클릭합니다.
빌드 메뉴에서 솔루션 다시 빌드를 클릭합니다.
Debugger.Break(C++ 샘플의 경우 DebugBreak)가 4번 호출됩니다. 따라서 중단점을 삽입할 필요가 없으며 응용 프로그램을 실행하기만 하면 디버거에서 응용 프로그램이 최대 4번 중단됩니다.
병렬 스택 창 사용: 스레드 뷰
디버그 메뉴에서 디버깅 시작을 클릭합니다. 첫 번째 중단점이 적중될 때까지 기다립니다.
단일 스레드의 호출 스택을 보려면
디버그 메뉴에서 창을 가리킨 다음 스레드를 클릭합니다. Visual Studio 아래쪽에 스레드 창을 고정합니다.
디버그 메뉴에서 창을 가리킨 다음 호출 스택을 클릭합니다. Visual Studio 아래쪽에 호출 스택 창을 고정합니다.
스레드 창에서 스레드를 두 번 클릭하여 활성화합니다. 활성화된 현재 스레드에는 노란색 화살표가 표시됩니다. 현재 스레드를 변경하는 경우 해당 호출 스택이 호출 스택 창에 표시됩니다.
병렬 스택 창을 검사하려면
디버그 메뉴에서 창을 가리키고 병렬 스택을 클릭합니다. 왼쪽 맨 위의 상자에서 스레드가 선택되어 있는지 확인합니다.
병렬 스택 창을 사용하여 한 뷰에서 동시에 여러 호출 스택을 볼 수 있습니다. 다음 그림은 호출 스택 창 위에 있는 병렬 스택 창을 보여 줍니다.
주 스레드의 호출 스택이 한 상자에 표시되고 다른 4개의 스레드에 대한 호출 스택이 다른 상자에서 그룹화됩니다. 4개의 스레드는 해당 스택 프레임이 동일한 메서드 컨텍스트를 공유 즉, 동일한 메서드인 A, B 및 C에 있기 때문에 그룹화됩니다. 동일한 상자를 공유하는 스레드의 이름과 스레드 ID를 보려면 머리글(4개 스레드)을 가리킵니다. 다음 그림과 같이 현재 스레드가 굵게 표시됩니다.
노란색 화살표는 현재 스레드의 활성 스택 프레임을 나타냅니다. 자세한 정보를 보려면 해당 항목을 가리킵니다.
호출 스택 창을 마우스 오른쪽 단추로 클릭하여 스택 프레임에 대해 표시할 정보(모듈 이름, 매개 변수 형식, 매개 변수 이름, 매개 변수 값, 줄 번호 및 바이트 오프셋)를 설정할 수 있습니다.
상자 주변의 파란색 강조 표시는 현재 스레드가 해당 상자의 일부임을 나타냅니다. 현재 스레드는 도구 설명에서 굵은 스택 프레임으로도 표시됩니다. 스레드 창에서 주 스레드를 두 번 클릭하면 그에 따라 병렬 스택 창의 파란색 강조 표시가 이동하는 것을 볼 수 있습니다.
2번째 중단점까지 실행을 계속하려면
2번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다. 다음 그림에서는 2번째 중단점에서의 스레드 트리를 보여 줍니다.
첫 번째 중단점에서 4개 스레드가 모두 S.A 메서드에서 S.B 메서드로 이동되고 다시 S.C 메서드로 이동되었습니다. 해당 정보가 병렬 스택 창에 계속 표시되지만 4개 스레드는 더 진행되었습니다. 그 중 하나가 S.D와 S.E로 진행되었습니다. 다른 하나는 S.F, S.G 및 S.H로 진행되었습니다. 나머지 두 개는 S.I와 S.J로 진행되었으며, 이 중단점에서 스레드 중 하나가 S.K로 이동되었고 다른 하나는 사용자 코드가 아닌 외부 코드로 진행되었습니다.
상자 머리글(1개 스레드 또는 2개 스레드)을 가리키면 스레드의 ID를 볼 수 있습니다. 스택 프레임을 가리키면 스레드 ID와 기타 스레드 정보를 볼 수 있습니다. 파란색 강조 표시는 현재 스레드를 나타내며 노란색 화살표는 현재 스레드의 활성 스택 프레임을 나타냅니다.
실 모양 아이콘(겹친 파란색 및 빨간색 물결선)은 현재 스레드가 아닌 스레드의 활성 스택 프레임을 나타냅니다. 호출 스택 창에서 S.B를 두 번 클릭하여 프레임을 전환합니다. 병렬 스택 창은 구부러진 녹색 화살표 아이콘을 사용하여 현재 스레드의 현재 스택 프레임을 나타냅니다.
스레드 창에서 스레드 간을 전환하고 병렬 스택 창의 뷰가 업데이트되는지 살펴봅니다.
병렬 스택 창에서 바로 가기 메뉴를 사용하여 다른 스레드로 전환하거나 다른 스레드의 다른 프레임으로 전환할 수 있습니다. 예를 들어, S.J를 마우스 오른쪽 단추로 클릭하고 프레임으로 전환을 가리킨 다음 명령을 클릭합니다.
S.C를 마우스 오른쪽 단추로 클릭하고 프레임으로 전환을 가리킵니다. 명령 중 하나에 현재 스레드의 스택 프레임을 나타내는 선택 표시가 있습니다. 동일한 스레드의 해당 프레임으로 전환하거나(녹색 화살표만 이동) 다른 스레드로 전환할 수 있습니다(파란색 강조 표시도 이동). 다음 그림에서는 하위 메뉴를 보여 줍니다.
메서드 컨텍스트가 하나의 스택 프레임에만 연결된 경우 상자 머리글에 1개 스레드가 표시되고 두 번 클릭하여 이 스레드로 전환할 수 있습니다. 두 개 이상의 프레임이 연결된 메서드 컨텍스트를 두 번 클릭하면 메뉴가 자동으로 표시됩니다. 메서드 컨텍스트를 가리키면 오른쪽에 검은색 삼각형이 나타납니다. 이 삼각형을 클릭해도 바로 가기 메뉴가 표시됩니다.
스레드가 많은 대규모 응용 프로그램의 경우 일부 스레드에만 초점을 맞출 수 있습니다. 병렬 스택 창에서는 플래그가 설정된 스레드에 대해서만 호출 스택을 표시할 수 있습니다. 도구 모음에서 목록 상자 옆에 있는 플래그 설정된 항목만 표시 단추를 클릭합니다.
그런 다음 스레드 창에서 각 스레드에 플래그를 설정하여 병렬 스택 창에서 해당 호출 스택이 표시되는 방식을 봅니다. 스레드에 플래그를 설정하려면 바로 가기 메뉴 또는 스레드의 첫 번째 셀을 사용합니다. 플래그 설정된 항목만 표시 도구 모음 단추를 다시 클릭하여 모든 스레드를 표시합니다.
3번째 중단점까지 실행을 계속하려면
3번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다.
여러 스레드가 동일한 메서드에 있지만 메서드가 호출 스택의 시작 지점에 있지 않은 경우 메서드가 서로 다른 상자에 나타납니다. 현재 중단점의 예는 S.L입니다. 이 중단점은 3개의 스레드를 포함하며 3개의 상자에 나타납니다. S.L을 두 번 클릭합니다.
다른 두 상자에서 S.L이 굵게 표시되므로 해당 위치를 확인할 수 있습니다. S.L을 호출되는 프레임과 S.L이 호출하는 프레임을 확인하려면 도구 모음에서 메서드 뷰 설정/해제 단추를 클릭합니다. 다음 그림에서는 병렬 스택 창의 메서드 뷰를 보여 줍니다.
다이어그램이 선택한 메서드를 축으로 회전하고 뷰 중간에 있는 자체 상자에 배치됩니다. 호출 수신자와 호출자가 맨 위와 맨 아래에 나타납니다. 메서드 뷰 설정/해제 단추를 다시 클릭하여 이 모드를 종료합니다.
병렬 스택 창의 바로 가기 메뉴에는 다음과 같은 기타 항목도 있습니다.
16진수 표시는 도구 설명의 숫자를 10수 또는 16진수로 전환합니다.
기호 로드 정보와 기호 설정은 해당 대화 상자를 엽니다.
소스 코드로 이동 및 디스어셈블리로 이동은 편집기에서 선택한 메서드로 이동합니다.
외부 코드 표시는 사용자 코드에 없는 프레임을 비롯한 모든 프레임을 표시합니다. 이 메뉴 항목을 사용하면 추가 프레임을 수용할 수 있게 다이어그램이 확장됩니다. 추가 프레임에 대한 기호가 없어서 프레임이 흐리게 표시될 수 있습니다.
큰 다이어그램이 있는 경우 다음 중단점으로 한 단계씩 코드를 실행할 때 뷰가 현재 스레드의 활성 스택 프레임(중단점을 처음 적중하는 스레드)으로 자동 스크롤되게 할 수 있습니다. 병렬 스택 창에서 도구 모음의 현재 스택 프레임으로 자동 스크롤 단추가 켜져 있는지 확인합니다.
계속하기 전에 병렬 스택 창에서 왼쪽과 아래쪽으로 스크롤합니다.
4번째 중단점까지 실행을 계속하려면
4번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다.
뷰가 어떻게 자동 스크롤되는지 봅니다. 스레드 창에서 스레드를 전환하거나 호출 스택 창에서 스택 프레임을 전환할 때도 뷰가 항상 정확한 프레임으로 자동 스크롤됩니다. 현재 도구 프레임으로 자동 스크롤 옵션을 끄고 차이를 확인합니다.
부감 뷰도 병렬 스택 창에서 큰 다이어그램을 사용할 때 도움이 됩니다. 다음 그림과 같이 창의 오른쪽 맨 아래에 있는 스크롤 막대 사이의 단추를 클릭하여 부감 뷰를 볼 수 있습니다.
사각형을 다이어그램 주변에서 빠르게 이동할 수 있습니다.
사각형을 한 방향으로 이동하는 또 한 가지 방법은 다이어그램의 검은색 영역을 클릭하여 원하는 위치로 끄는 것입니다.
다이어그램을 확대하거나 축소하려면 Ctrl 키를 누른 상태에서 마우스 휠을 움직입니다. 또는 도구 모음의 확대/축소 단추를 클릭하고 확대/축소 도구를 사용합니다.
도구 메뉴를 클릭하고 옵션을 클릭한 다음 디버깅 노드 아래의 옵션을 선택하거나 선택 취소하여 상향식이 아닌 하향식으로 스택을 볼 수도 있습니다.
계속하기 전에 디버그 메뉴에서 디버깅 중지를 클릭하여 실행을 종료합니다.
병렬 작업 창과 병렬 스택 창의 작업 뷰 사용
계속하기 전에 이전 절차를 완료하는 것이 좋습니다.
첫 번째 중단점이 적중될 때까지 응용 프로그램을 다시 시작하려면
디버깅 메뉴에서 디버깅 시작을 클릭하고 첫 번째 중단점이 적중될 때까지 기다립니다.
디버그 메뉴에서 창을 가리킨 다음 스레드를 클릭합니다. Visual Studio 아래쪽에 스레드 창을 고정합니다.
디버그 메뉴에서 창을 가리키고 호출 스택을 클릭합니다. Visual Studio 아래쪽에 호출 스택 창을 고정합니다.
스레드 창에서 스레드를 두 번 클릭하여 활성화합니다. 활성화된 현재 스레드에는 노란색 화살표가 표시됩니다. 현재 스레드를 변경하면 다른 창이 업데이트됩니다. 다음에는 작업을 살펴보겠습니다.
디버그 메뉴에서 창을 가리킨 다음 병렬 작업을 클릭합니다. 다음 그림에서는 병렬 작업 창을 보여 줍니다.
실행 중인 각 작업에 대해 같은 이름의 속성에서 반환되는 작업의 ID, 작업을 실행하는 스레드의 ID 및 이름, 작업의 위치가 표시됩니다. 작업을 가리키면 전체 호출 스택이 포함된 도구 설명이 표시됩니다. 또한 작업 열 아래에서 작업에 전달된 메서드 즉, 시작 지점을 볼 수 있습니다.
열을 정렬할 수 있습니다. 정렬 문자 모양이 정렬 열과 방향을 나타냅니다. 열을 왼쪽이나 오른쪽으로 끌어서 열을 다시 정렬할 수도 있습니다.
노란색 화살표는 현재 작업을 나타냅니다. 작업을 두 번 클릭하거나 바로 가기 메뉴를 사용하여 작업을 전환할 수 있습니다. 작업을 전환하면 주 스레드가 현재 스레드가 되고 다른 창이 업데이트됩니다.
작업 간을 수동으로 전환하는 경우 노란색 화살표는 이동하지만 흰색 화살표는 디버거를 중단시킨 작업을 계속 표시합니다.
2번째 중단점까지 실행을 계속하려면
2번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다.
이전에는 상태 열에 모든 작업이 실행 중으로 표시되었지만 이제는 작업 중 두 개가 대기 중입니다. 작업은 다양한 이유로 차단될 수 있습니다. 상태 열에서 대기 중인 작업을 가리키면 해당 작업이 차단된 이유를 알 수 있습니다. 예를 들어, 다음 그림에서 작업 3이 작업 4를 기다리고 있습니다.
작업 4는 작업 2에 할당된 스레드가 소유하는 모니터를 기다리고 있습니다.
병렬 작업 창에서 첫 번째 열의 플래그를 클릭하여 작업에 플래그를 설정할 수 있습니다.
플래그 설정을 사용하여 동일한 디버깅 세션의 여러 중단점 간에 작업을 추적하거나 병렬 스택 창에 호출 스택이 표시되는 작업을 필터링할 수 있습니다.
앞에서 병렬 스택 창을 사용할 때 응용 프로그램 스레드를 보았습니다. 이번에는 병렬 작업 창에서 응용 프로그램 작업을 봅니다. 이렇게 하려면 왼쪽 위의 상자에서 작업을 선택합니다. 다음 그림에서는 작업 뷰를 보여 줍니다.
현재 실행 중인 작업이 아닌 스레드는 병렬 스택 창의 작업 뷰에 표시되지 않습니다. 또한 작업을 실행하는 스레드의 경우 작업과 관련이 없는 스택 프레임 중 일부가 스택의 맨 위와 맨 아래에서 필터링됩니다.
병렬 작업 창을 다시 봅니다. 열 머리글을 마우스 오른쪽 단추로 클릭하여 열의 바로 가기 메뉴를 표시합니다.
바로 가기 메뉴를 사용하여 열을 추가하거나 제거할 수 있습니다. 예를 들어, AppDomain 열은 선택되지 않았으므로 목록에 표시되지 않습니다. 부모를 클릭합니다. 4개 작업 중 하나에 대해 부모 열이 값 없이 표시됩니다.
3번째 중단점까지 실행을 계속하려면
3번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다.
지금 새 작업인 작업 5가 실행 중이며 작업 4가 대기 중입니다. 상태 창에서 대기 중인 작업을 가리키면 해당 작업이 대기 중인 이유를 알 수 있습니다. 부모 열에서 작업 4는 작업 5의 부모입니다.
부모-자식 관계를 시각적으로 표현하려면 부모 열 머리글을 오른쪽 단추로 클릭하고 부모 자식 뷰를 클릭합니다. 다음 그림이 표시됩니다.
동일한 스레드에 대해 작업 4와 작업 5가 실행되고 있습니다. 이 정보는 스레드 창에 표시되지 않으므로 여기에서 이 정보를 볼 수 있다는 것은 병렬 작업 창의 또 한 가지 이점입니다. 이를 확인하려면 병렬 스택 창을 봅니다. 작업을 보고 있는지 확인합니다. 병렬 작업 창에서 작업 4와 5를 두 번 클릭하여 해당 작업을 찾습니다. 이때 병렬 스택 창의 파란색 강조 표시가 업데이트됩니다. 병렬 스택 창에서 도구 설명을 검색하여 작업 4와 5를 찾을 수도 있습니다.
병렬 스택 창에서 S.P를 마우스 오른쪽 단추로 클릭하고 스레드로 이동을 클릭합니다. 창이 스레드 뷰로 전환되고 해당 프레임이 뷰에 표시됩니다. 동일한 스레드에서 두 작업을 볼 수 있습니다.
이는 스레드 창과 비교하여 병렬 스택 창의 작업 뷰를 사용할 때의 또 한 가지 이점입니다.
4번째 중단점까지 실행을 계속하려면
3번째 중단점이 적중될 때까지 실행을 계속하려면 디버그 메뉴에서 계속을 클릭합니다. ID 열 머리글을 클릭하여 ID순으로 정렬합니다. 다음 그림이 표시됩니다.
작업 5가 완료되어 더 이상 표시되지 않습니다. 그렇지 않고 교착 상태도 표시되지 않으면 F11 키를 눌러 한 단계씩 코드를 실행합니다.
작업 3과 작업 4가 서로 대기 중이어서 교착 상태에 있습니다. 또한 작업 2의 자식인 5개의 새 작업이 있고 현재 예약되어 있습니다. 예약된 작업은 코드 내에서는 시작되었지만 아직 실행되지 않은 작업입니다. 따라서 위치와 스레드 할당 열이 비어 있습니다.
병렬 스택 창을 다시 봅니다. 각 상자의 머리글에는 스레드 ID 및 이름을 표시하는 도구 설명이 있습니다. 병렬 스택 창에서 작업 뷰로 전환합니다. 그림과 같이 머리글을 가리키면 작업 ID, 이름 및 상태가 표시됩니다.
열별로 작업을 그룹화할 수 있습니다. 병렬 작업 창에서 상태 열 머리글을 오른쪽 단추로 클릭하고 상태별로 그룹화를 클릭합니다. 다음 그림에서는 상태별로 그룹화된 병렬 작업 창을 보여 줍니다.
다른 열을 기준으로 그룹화할 수도 있습니다. 작업을 그룹화하여 일부 작업에만 초점을 맞출 수 있습니다. 축소 가능한 그룹마다 함께 그룹화되는 항목 수가 있습니다. 또한 축소 단추의 오른쪽에 있는 플래그 설정 단추를 클릭하여 그룹의 모든 항목에 빠르게 플래그를 설정할 수 있습니다.
마지막으로 살펴볼 병렬 작업 창의 기능은 작업을 오른쪽 단추로 클릭할 때 표시되는 바로 가기 메뉴입니다.
바로 가기 메뉴에는 작업 상태에 따라 다양한 명령이 표시됩니다. 명령에는 복사, 모두 선택, 16진수 표시, 작업으로 전환, 할당된 스레드 중지, 이 스레드를 제외한 모든 스레드 중지, 할당된 스레드 재개 및 플래그 설정이 있습니다.
작업의 내부 스레드를 중지하거나 할당된 스레드를 제외한 모든 스레드를 중지할 수 있습니다. 중지된 스레드는 스레드 창에서처럼 병렬 작업 창에 표시되며 옆에 파란색 일시 중지 아이콘이 있습니다.
요약
이 연습에서는 병렬 작업 및 병렬 스택 디버거 창에 대해 설명했습니다. 다중 스레드 코드를 사용하는 실제 프로젝트에서 이러한 창을 사용하십시오. C++, C# 또는 Visual Basic으로 작성된 병렬 코드를 검사할 수 있습니다.