Parallel

Multithreaded Debugging Techniques

By Shameem Akhter and Jason Roberts, April 23, 2007

Debugging multithreaded applications can be a challenging task.

Debugging Multithreaded Applications in Windows

Most Windows programmers use Microsoft Visual Studio as their primary integrated development environment (IDE). As part of the IDE, Microsoft includes a debugger with multithreaded debug support. This section examines the different multithreaded debug capabilities of Visual Studio, and then demonstrates how they are used.

Threads Window

As part of the debugger, Visual Studio provides a "Threads" window that lists all of the current threads in the system. From this window, you can:

Freeze (suspend) or thaw (resume) a thread. This is useful when you want to observe the behavior of your application without a certain thread running.

Switch the current active thread. This allows you to manually perform a context switch and make another thread active in the application.

Examine thread state. When you double-click an entry in the Threads window, the source window jumps to the source line that the thread is currently executing. This tells you the thread's current program counter. You will be able to examine the state of local variables within the thread.

The Threads window acts as the command center for examining and controlling the different threads in an application.

Tracepoints

As previously discussed, determining the sequence of events that lead to a race condition or deadlock situation is critical in determining the root cause of any multithread related bug. In order to facilitate the logging of events, Microsoft has implemented tracepoints as part of the debugger for Visual Studio 2005.

Most developers are familiar with the concept of a breakpoint. A tracepoint is similar to a breakpoint except that instead of stopping program execution when the applications program counter reaches that point, the debugger takes some other action. This action can be printing a message or running a Visual Studio macro.

Enabling tracepoints can be done in one of two ways. To create a new tracepoint, set the cursor to the source line of code and select "Insert Tracepoint." If you want to convert an existing breakpoint to a tracepoint, simply select the breakpoint and pick the "When Hit" option from the Breakpoint submenu. At this point, the tracepoint dialog appears.

When a tracepoint is hit, one of two actions is taken based on the information specified by the user. The simplest action is to print a message. The programmer may customize the message based on a set of predefined keywords. These keywords, along with a synopsis of what gets printed, are shown in Table 1. All values are taken at the time the tracepoint is hit.

[Click image to view at full size]

Table 1: Tracepoint Keywords

In addition to the predefined values in Table 1, tracepoints also give you the ability to evaluate expressions inside the message. In order to do this, simply enclose the variable or expression in curly braces. For example, assume your thread has a local variable threadLocalVar that you'd like to have displayed when a tracepoint is hit. The expression you'd use might look something like this:

Thread: $TNAME local variables value is {threadLocalVar}.

Breakpoint Filters

Breakpoint filters allow developers to trigger breakpoints only when certain conditions are triggered. Breakpoints may be filtered by machine name, process, and thread. The list of different breakpoint filters is shown in Table 2.

[Click image to view at full size]

Table 2: Breakpoint Filter Options

Breakpoint filters can be combined to form compound statements. Three logic operators are supported: !(NOT), &(AND), and ||(OR).

Naming Threads

When debugging a multithreaded application, it is often useful to assign unique names to the threads that are used in the application. Assigning a name to a thread in a managed application is as simple as setting a property on the thread object. In this environment, it is highly recommended that you set the name field when creating the thread, because managed code provides no way to identify a thread by its ID.

In native Windows code, a thread ID can be directly matched to an individual thread. Nonetheless, keeping track of different thread IDs makes the job of debugging more difficult; it can be hard to keep track of individual thread IDs. You may have noticed the conspicuous absence of any sort of name parameter in the methods used to create threads. In addition, there is no function provided to get or set a thread name. It turns out that the standard thread APIs in Win32 lack the ability to associate a name with a thread. As a result, this association must be made by an external debugging tool.

Microsoft has enabled this capability through predefined exceptions built into their debugging tools. Applications that want to see a thread referred to by name need to implement a small function that raises an exception. The exception is caught by the debugger, which then takes the specified name and assigns it to the associated ID. Once the exception handler completes, the debugger will use the user-supplied name from then on.

The implementation of this function can be found on the Microsoft Developer Network (MSDN) Web site at msdn.microsoft.com by searching for: "setting a thread name (unmanaged)." The function, named SetThreadName(), takes two arguments. The first argument is the thread ID. The recommended way of specifying the thread ID is to send the value -1, indicating that the ID of the calling thread should be used. The second parameter is the name of the thread. The SetThreadName() function calls RaiseException(), passing in a special 'thread exception' code and a structure that includes the thread ID and name parameters specified by the programmer.

Once the application has the SetThreadName() function defined, the developer may call the function to name a thread. This is shown in Listing Five. The function Thread1 is given the name Producer, indicating that it is producing data for a consumer (Admittedly the function name Thread1 should be renamed to Producer as well, but is left somewhat ambiguous for illustration purposes. Note that the function is called at the start of the thread, and that the thread ID is specified as -1. This indicates to the debugger that it should associate the calling thread with the associated ID.

Naming a thread in this fashion has a couple of limitations. This technique is a debugger construct; the OS is not in any way aware of the name of the thread. Therefore, the thread name is not available to anyone other than the debugger. You cannot programmatically query a thread for its name using this mechanism. Assigning a name to a thread using this technique requires a debugger that supports exception number 0x406D1388. Both Microsoft's Visual Studio and WinDbg debuggers support this exception. Despite these limitations, it is generally advisable to use this technique where supported as it makes using the debugger and tracking down multithreaded bugs much easier.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!