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.
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.
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.
Listing Five: Using SetThreadName to Name a Thread.
unsigned __stdcall Thread1(void *) { int i, x = 0; // arbitrary local variable declarations SetThreadName(-1, "Producer"); // Thread logic follows }
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.
Related Reading
More Insights
To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy. | |