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@wintellect.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.Breakpoint2
        For Each currBP In DTE.Debugger.Breakpoints
            ' Only set the filter if it's empty.
            If (String.IsNullOrEmpty(currBP.FilterBy) = True) Then
                currBP.FilterBy = k_THREADFILTER
            End If
        Next
    End Sub

    ' For all breakpoints, clears the Filter if it's set to 
    ' "ThreadName == InterestingThread"
    Public Sub ClearAllBreakpointInterestingThreadFilters()
        Dim currBP As EnvDTE80.Breakpoint2
        For Each currBP In DTE.Debugger.Breakpoints
            If (String.Compare(currBP.FilterBy, k_THREADFILTER) = 0) Then
                currBP.FilterBy = String.Empty
            End If
        Next
    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) Then
            Exit Sub
        End 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.Selection
        Dim point As VirtualPoint = txtSel.TopPoint

        Try
            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.Breakpoint2
            For Each newBP In bps
                newBP.FilterBy = k_THREADFILTER
            Next
        Catch ex As COMException
            ErrorMessage(ex.Message)
        End Try

    End Sub

    Private 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) Then
            ErrorMessage("You must have a project open.")
            GetCurrentDocument = Nothing
            Exit Function
        End 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.ActiveWindow
        Dim currWinDoc As Document = currWin.Document
        Dim 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.Empty
        If Not (currWinDoc Is Nothing) Then
            winDocName = currWinDoc.Name
        End If
        Dim docName As String = "x"
        If Not (currDoc Is Nothing) Then
            docName = currDoc.Name
        End If

        If ((currWinDoc Is Nothing) And _
            (winDocName <> docName)) Then
            ErrorMessage("The active cursor is not in a code document.")
            GetCurrentDocument = Nothing
            Exit Function
        End 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.FileCodeModel
        If (fileMod Is Nothing) Then
            ErrorMessage("Unable to get code model from document.")
            GetCurrentDocument = Nothing
            Exit Function
        End If

        GetCurrentDocument = currDoc
    End Function

    Private 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.Handle
            Get
                ' The HWnd property is undocumented.
                Dim ret As IntPtr = DTE.MainWindow.HWnd
                Return (ret)
            End Get
        End Property
    End Class

End Module