Debugging Program State

The best approach is to use the Visual Studio debugger, which contains some very useful new parallel features to support parallel programming.

To look at these features, we are going to use the code in Listing 7-4. This program has no useful value other than it causes a number of Tasks to call a number of methods to demonstrate the debugger features. The CountDownEvent is used so that the main application thread can wait until all of the Tasks have been created and scheduled. The SemaphoreSlims are included so that two Tasks can gain access to the critical regions in TerminalMethodA() and TerminalMethodB().

Listing 7-4 throws an exception, because I want to be able to include code for you to download or reproduce that will behave exactly as described here. Usually, I would set a breakpoint by right-clicking in Visual Studio and selecting Breakpoint > Insert Breakpoint.

Listing 7-4. Exercising the Parallel Debugger using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks;

namespace Listing_04 {

class Listing_04 {

static CountdownEvent cdEvent; static SemaphoreSlim semA, semB;

static void Main(string[] args) { // initialize the semaphores semA = new SemaphoreSlim(2); semB = new SemaphoreSlim(2);

// define the number of tasks we will use int taskCount = 10;

// initialize the barrier cdEvent = new CountdownEvent(taskCount);

tasks[i] = Task.Factory.StartNew((stateObject) => {

InitialMethod((int)stateObject); }, i);

// wait for all of the tasks to have reached a terminal method cdEvent.Wait();

// throw an exception to force the debugger to break throw new Exception();

static void InitialMethod(int argument) { if (argument % 2 == 0) {

MethodA(argument); } else {

MethodB(argument);

static void MethodA(int argument) { if (argument < 5) {

TerminalMethodA(); } else {

TerminalMethodB();

static void MethodB(int argument) { if (argument < 5) {

TerminalMethodA(); } else {

TerminalMethodB();

static void TerminalMethodA() { // signal the countdown event cdEvent.Signal();

// acquire the lock for this method semA.Wait(); // perform some work for (int i = 0; i < 500000000; i++) { Math.Pow(i, 2);

// release the semaphore semA.Release();

static void TerminalMethodB() { // signal the countdown event cdEvent.Signal();

// acquire the lock for this method semB.Wait(); // perform some work for (int i = 0; i < 500000000; i++) { Math.Pow(i, 3);

To see the parallel debugging features, compile the code in Listing 7-4, and select Start Debugging from the Debug menu. Once the Tasks have been created and reached the required point, the main application threads will throw an exception, and the debugger will break.

The first new feature to look at is the Parallel Tasks window, which you can display by selecting Parallel Tasks from the Debug > Windows menu. This window shows all of the Tasks in your code, as illustrated in Figure 7-7.

Parallel Tasks

ID

Status

Location

Task

Thread Assignment

Y" -V

1

■^i) Running

Listmg_04.l_isting_04.TermmalMethodA

Main.Anonyrnou&Method_

JW

6700 [WorkerThread)

2

Running

Li stin g_04. Li sti n g_04 .Term in a 1M eth o d A

Ma in. Anonym ousMethod_

9828 [WorkerThread)

3

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodA

Main.AnonymousMethod_

-OP)

8672 [WorkerThread)

4

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodA

Main.AnonymousMethod_

-OP)

5896 [WorkerThread)

5

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodA

Main.AnonymousMethod_

JW

8480 [WorkerThread)

6

Running

Li:ting_04.Li:ting_04.TerminalMethodB

Main.AnonymousMethod_

JUS

7748 [WorkerThread)

7

Running

Li:ting_04.Li:ting_04.TerminalMethodB

Main.AnonymousMethod_

_0[6)

6148 [WorkerThread)

S

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodB

Main.AnonymousMethod_

J>P)

5228 [WorkerThread)

9

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodB

Main.AnonymousMethod_

236 [Worker Thread)

7

10

? Waiting

Li:ting_04.Li:ting_04.TerminalMethodB

Main.AnonymousMethod_

_0p)

3540 [WorkerThread)

"C

Output m Locab JQ Watch 1

Lgl Parallel Tasks I

Figure 7-7. The Parallel Tasks debugger window

For Listing 7-4, all ten Tasks are listed in Figure 7-7, and the Task ID, the current status and other information is displayed for each. The currenly viewed Task is indicated by the yellow arrow (if you don't see a yellow arrow, double-click the first task in the list); double-clicking a Task in the list makes that the current Task. The currently viewed Task's call stack is displayed in the Call Stack debugger window and is also highlighted in the Parallel Stacks window, as you'll see in a moment.

You can select columns to display diferent Task information by right-clicking the column headers. Table 7-2 describes the available columns.

Table 7-2. Parallel Tasks Columns

Column Description

Flags Right-clicking the flag icon allows you to flag or unflag a Task. The Parallel Stacks window allows you to view only flagged Tasks to simplify the display.

Icons A yellow arrow shows the currently viewed Task. A white arrow shows the Task that caused the debugger to break. A pause icon shows that a Task is frozen.

ID This is the Task ID, which is equivalent to the result from the Task.CurrentId property.

Status This is the status of the Task prior to the debugger breaking. "Running" means that the Task was executing. "Waiting" means that the Task was waiting to acquire a synchronization primitive or waiting for another Task to complete. A scheduled Task has been created and is awaiting execution. A deadlocked Task is one that is engaged in a deadlock with another Task; see the "Detecting Deadlocks" section later in this chapter for details.

Location This is the location in the call stack of the Task. You can see the entire call stack by hovering over this field with the mouse.

Task The initial method and arguments that were used to create the Task.

Parent This is the ID of the parent Task if there is one.

Thread Assignment This is the name of the classic thread that is executing the Task. Note that a thread can be used to perform more than one Task and that a Task may be performed inline (see Chapter 4 for details of inline execution).

You can group the Tasks in the list by any of the columns, so you can see all of the Tasks with a given status or were executing a given method when the debugger broke. As you step through the code with a debugger, you can switch from Task to Task.

You can freeze a Task by right-clicking and selecting Freeze Thread. The Task will no longer execute when you step through the code. However, it is the classic thread executing the Task that is frozen, not just the Task, so some care should be taken when using this feature.

The other new window is the Parallel Stacks display, which you can select from the Debug > Windows menu. Figure 7-8 shows this window for Listing 7-4.

Figure 7-8. The Parallel Stacks debugger window

Make sure that you have selected Tasks from the drop-down list at the top-left of the window. This debugger window shows you a graphical representation of all of your Tasks that is focused on the methods that they have called. You can see that of the ten Tasks created in Listing 7-4, five have called MethodA(), five have called MethodB(), and so on down through the call stack. The buttons across the top of the window allow you to display only Tasks you have flagged in the Parallel Tasks window and to focus on one specific method, rather than showing the entire call tree. If you hover over the title of each box, you will see details of the Tasks that have called it, and you can switch to any of them.

As you switch from Task to Task, the code window will show the source statements for the current Task. The Call Stack switches to the calls made by that Task; the Locals window shows the data for the current Task, and so on.

By using the new parallel windows, you can drive the debugger from Task to Task effortlessly. You can see what each Task was doing when the debugger broke, see the local data for each Task, and walk through Task-by-Task as the code executes. Unlike the Concurrency Visualizer, which is an awkward tool, the parallel debugger features are fast and elegant, and they allow you to dig deep into the detail of your parallel program.

0 0

Post a comment