Last week I was working at Symantec and one of the people I was working with asked me the following: “When you’re debugging heavily multithreaded applicatons, Visual Studio doesn’t handle it too well. You can’t see multiple thread call stacks and locals windows at the same time. Worst of all, once you find that interesting thread you want to focus on there’s no easy way to change all your breakpoints to stop just on that thread. Is there any way to fix these problems?”
For looking at multiple call stacks, there’s nothing we can do today, but Visual Studio 2010 will have the new Parallel Stacks window, as shown below, which will show you not only the call stacks, but the relationships between them. As for multiple Local windows, that won’t be showing up any time soon.
While Visual Studio 2008 does allow you to add a Filter to a breakpoint, where you can stop on a thread id, thread name, process name, process id, and machine name, you have to set the breakpoint manually. As soon as I was asked if there was a way to change all your breakpoints to stop on a specific thread, I knew there was enough flexibility in Visual Studio 2008 to do so. Thinking about all the times I could have used something like this, I knew others would find it useful so here you go. Just to please everyone, these macros work for both .NET and native C++ debugging.
At the end of this article is the code for a set of Visual Studio 2008 macros I wrote that make multithreaded debugging easier. If you’re not familiar with creating and running Visual Studio macros, I’ll refer you to the documentation. For the rest of the article, I’ll assume you have the macros properly loaded and running.
As you’re debugging your multithreaded program, you’ll narrow down the thread that looks interesting and you want to switch all your breakpoints to stop only on that thread and no others. While I could use the thread ID with a Filter breakpoint, as you’ll see in a little bit, that would be limiting so it’s better to use a thread name. Fortunately, Visual Studio makes it easy to set a thread name by right clicking on the thread in the Threads window and selecting Rename.
In the Name column, type in “InterestingThread” (notice there’s no space between the words). To change all existing breakpoints to stop only on the named thread, double click my SetAllBreakpointsToInterestingThread in the Macro Explorer. That will change all breakpoints that do not have a Filter set to “ThreadName == InterestingThread” so you will only stop on that thread. That’s all there is to telling the debugger to stop on that one thread.
If you want to remove the Filter from your breakpoints, double click on the ClearAllBreakpointInterestingThread macro. If you’d like to specifically set a breakpoint that has the “ThreadName == InterestingThread” filter applied, use the SetInterestingThreadBreakpoint. The one macro I wanted to add one that renamed the current thread to “InterestingThread” but sadly, the debugger automation model does not expose the thread naming setter function. Maybe we can get that in Visual Studio 2010.
One thing I noticed testing these macros is that if you are not debugging setting the Filter does not properly show up in Breakpoints window. However, the breakpoints do show up with the white cross and the tool tip on the margin glyph shows the filter. Once you start debugging, the Breakpoint window properly updates itself.
Some of you are thinking that these macros look cool, but sometimes you have two or more threads where you need to stop. How’s this for super cool; the debugger happily lets you set multiple threads to the same name in the Threads window. That means you just need to rename all the threads you want to stop on to “InterestingThread” to stop on all your, well, interesting threads!
I was glad to at least answer part of the question. It’s great that the debugger team has exposed plenty through the automation model that it isn’t too hard to add functionality like this without too much work. Is there something that you’d like to see the debugger or Visual Studio do that it doesn’t? Don’t hesitate to ask in the comments or send me an email and I’ll be happy to tackle it.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' InterestingThread - John Robbins (c) 2009 - john@training.atmosera.com
' A set of macros that make debugging multithreaded programs easier.
'
' Version 1.0 - July 11, 2009
' - Initial version.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Imports System
Imports EnvDTE
Imports EnvDTE80
Imports EnvDTE90
Imports System.Diagnostics
Imports System.Text
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Public Module InterestingThread' The caption for all message boxes.
Private Const captionTitle As String = _"Wintellect Interesting Thread Macros"
' The Filter set on breakpoints.
Private Const k_THREADFILTER As String = "ThreadName == InterestingThread"' For all breakpoints, sets the Filter to "ThreadName == InterestingThread"
Public Sub SetAllBreakpointsToInterestingThreadFilter()Dim currBP As EnvDTE80.Breakpoint2For Each currBP In DTE.Debugger.Breakpoints' Only set the filter if it's empty.
If (String.IsNullOrEmpty(currBP.FilterBy) = True) ThencurrBP.FilterBy = k_THREADFILTEREnd IfNext
End Sub' For all breakpoints, clears the Filter if it's set to
' "ThreadName == InterestingThread"
Public Sub ClearAllBreakpointInterestingThreadFilters()Dim currBP As EnvDTE80.Breakpoint2For Each currBP In DTE.Debugger.BreakpointsIf (String.Compare(currBP.FilterBy, k_THREADFILTER) = 0) ThencurrBP.FilterBy = String.Empty
End IfNext
End Sub' Sets a breakpoint on the current source code line with the Filter set
' to "ThreadName == InterestingThread"
Public Sub SetInterestingThreadBreakpoint()' There has to be a document open for things to work.
Dim currDoc As Document = GetCurrentDocument()If (currDoc Is Nothing) ThenExit SubEnd If' Get the cursor location. I'll make my setter behave the same
' way as when you have a selection and press F9, which will
' set a breakpoint on the top part of the selection. However,
' unlike F9 breakpoints, I won't clear the selection after
' setting the breakpoint.
Dim txtSel As TextSelection = currDoc.SelectionDim point As VirtualPoint = txtSel.TopPointTry
Dim bps As EnvDTE.Breakpoints = DTE.Debugger.Breakpoints.Add( _File:=currDoc.FullName, _Line:=point.Line, _Column:=point.DisplayColumn)' There's no way to set the filter property when adding a
' breakpoint so you have to do it after the file and line
' breakpoint is set.
Dim newBP As EnvDTE80.Breakpoint2For Each newBP In bpsnewBP.FilterBy = k_THREADFILTERNext
Catch ex As COMExceptionErrorMessage(ex.Message)End TryEnd SubPrivate Function GetCurrentDocument() As Document' Check to see if a project or solution is open. If not, you
' can't get at the code model for the file.
Dim projs As System.Array = CType(DTE.ActiveSolutionProjects, Array)If (projs.Length = 0) ThenErrorMessage("You must have a project open.")
GetCurrentDocument = Nothing
Exit FunctionEnd If' Getting the active document is a little odd.
' DTE.ActiveDocument will return the active code document, but
' it might not be the real ACTIVE window. It's quite
' disconcerting to see macros working on a document when you're
' looking at the Start Page. Anyway, I'll ensure the active
' document is really the active window.
Dim currWin As Window = DTE.ActiveWindowDim currWinDoc As Document = currWin.DocumentDim currDoc As Document = DTE.ActiveDocument' Gotta play the game to keep from null ref exceptions in the
' real active doc check below.
Dim winDocName As String = String.EmptyIf Not (currWinDoc Is Nothing) ThenwinDocName = currWinDoc.NameEnd IfDim docName As String = "x"If Not (currDoc Is Nothing) ThendocName = currDoc.NameEnd IfIf ((currWinDoc Is Nothing) And _(winDocName <> docName)) Then
ErrorMessage("The active cursor is not in a code document.")
GetCurrentDocument = Nothing
Exit FunctionEnd If' While I might have a document, I still need to check this is
' one I can get a code model from.
Dim fileMod As FileCodeModel = _currDoc.ProjectItem.FileCodeModelIf (fileMod Is Nothing) ThenErrorMessage("Unable to get code model from document.")
GetCurrentDocument = Nothing
Exit FunctionEnd IfGetCurrentDocument = currDocEnd FunctionPrivate Sub ErrorMessage(ByVal text As String)MessageBox.Show(New MainWindow(), _
text, _captionTitle, _MessageBoxButtons.OK, _MessageBoxIcon.Error)
End Sub' A helper class so I can parent message boxes correctly on the IDE.
Class MainWindow
Implements IWin32Window
Public ReadOnly Property Handle() _As System.IntPtr Implements IWin32Window.HandleGet
' The HWnd property is undocumented.
Dim ret As IntPtr = DTE.MainWindow.HWndReturn (ret)
End GetEnd PropertyEnd ClassEnd Module