开始调试多线程应用程序(C#、Visual Basic、C++)

Visual Studio 提供多种工具和用户界面元素,用于调试多线程应用程序。 本教程演示如何使用线程标记、“并行堆栈” 窗口、“并行监视” 窗口、条件断点、筛选器断点。 完成本教程可使你熟悉用于调试多线程应用程序的 Visual Studio 功能。

下面两篇文章额外介绍了如何使用其他多线程调试工具:

第一步是创建多线程应用程序项目。

创建一个多线程应用项目

  1. 打开 Visual Studio 并创建一个新项目。

    如果“开始”窗口未打开,请选择“文件”>“启动窗口”。

    在“开始”窗口上,选择“创建新项目”。

    在“创建新项目”窗口的搜索框中输入或键入“控制台” 。 接下来,从“语言”列表中选择“C#”、“C++”或“Visual Basic”,然后从“平台”列表中选择“Windows” 。

    应用语言和平台筛选器之后,对 .NET 或 C++ 选择“控制台应用”模板,然后选择“下一步”。

    注意

    如果没有看到正确的模板,请转到“工具”>“获取工具和功能...”,这会打开 Visual Studio 安装程序 。 选择“.NET 桌面开发”或“使用 C++ 的桌面开发”工作负载,然后选择“修改” 。

    在“配置新项目”窗口中,在“项目名称”框中键入或输入 MyThreadWalkthroughApp。 然后,选择“下一步”或“创建”,(视具体提供的选项而定)。

    对于 .NET Core 或 .NET 5+ 项目,选择建议的目标框架或 .NET 8,然后选择“创建”

    新的控制台项目随即显示。 创建该项目后,将显示源文件。 根据所选语言,源文件名称可能是 Program.cs、MyThreadWalkthroughApp.cpp 或 Module1.vb 。

  2. 删除源文件中显示的代码,并将其替换为以下更新的代码。 为代码配置选择适当的代码片段。

    using System;
    using System.Threading;
    
    public class ServerClass
    {
    
        static int count = 0;
        // The method that will be called when the thread is started.
        public void InstanceMethod()
        {
            Console.WriteLine(
                "ServerClass.InstanceMethod is running on another thread.");
    
            int data = count++;
            // Pause for a moment to provide a delay to make
            // threads more apparent.
            Thread.Sleep(3000);
            Console.WriteLine(
                "The instance method called by the worker thread has ended. " + data);
        }
    }
    
    public class Simple
    {
        public static void Main()
        {
            for (int i = 0; i < 10; i++)
            {
                CreateThreads();
            }
        }
        public static void CreateThreads()
        {
            ServerClass serverObject = new ServerClass();
    
            Thread InstanceCaller = new Thread(new ThreadStart(serverObject.InstanceMethod));
            // Start the thread.
            InstanceCaller.Start();
    
            Console.WriteLine("The Main() thread calls this after "
                + "starting the new InstanceCaller thread.");
    
        }
    }
    
  3. 在“文件” 菜单上,单击“全部保存” 。

  4. (仅适用于 Visual Basic)在“解决方案资源管理器”(右窗格)中,右键单击项目节点,然后选择“属性” 。 在“应用程序” 选项卡下,将“启动对象” 更改为“简单” 。

调试多线程应用

  1. 在源代码编辑器中,查找以下代码片段:

    Thread.Sleep(3000);
    Console.WriteLine();
    
  2. Thread.Sleep 语句或 std::this_thread::sleep_for 语句(针对 C++)的左滚动条槽中单击左键,以插入新的断点。

    在滚动条槽中,红色圆圈表示已在该位置设置了一个断点。

  3. 在“调试” 菜单上,单击“开始调试(F5)” 。

    Visual Studio 将生成该解决方案,应用在附加了调试器的情况下开始运行,然后在断点处停止。

  4. 在源代码编辑器中,找到包含断点的行。

发现线程标记

  1. 在调试工具栏中,单击“在源中显示线程”按钮 Show Threads in Source

  2. F11 两次以完成调试器的下一步。

  3. 查看窗口左侧的滚动条槽。 在此行中,注意“线程标记”图标 Thread Marker,类似于一条双绞线。 线程标记指示线程在此位置停止。

    线程标记可以被断点部分隐藏。

  4. 将指针悬停在线程标记上。 此时会出现一个数据提示,告知你每个已停止线程的名称和线程 ID 号。 在这种情况下,名称可能是 <noname>

    Screenshot of the Thread ID in a DataTip.

  5. 选择线程标记,以查看快捷菜单上的可用选项。

查看线程位置

在“并行堆栈” 窗口中,可以在“线程”视图和“任务”视图(适用于基于任务的编程)之间进行切换,并且可以查看每个线程的调用堆栈信息。 在此应用中,我们可以使用“线程”视图。

  1. 通过选择“调试”>“窗口”>“并行堆栈” ,打开“并行堆栈” 窗口。 此时会看到如下所示的内容。 确切信息可能因每个线程的当前位置、硬件和编程语言而异。

    Screenshot of the Parallel Stacks Window.

    在此示例中,从左到右会看到托管代码的以下信息:

    • 当前线程(黄色箭头)已进入 ServerClass.InstanceMethod。 可以通过将鼠标悬停在 ServerClass.InstanceMethod 上来查看线程的线程 ID 和堆栈帧。
    • 线程 31724 正在等待线程 20272 拥有的锁。
    • 主线程(左侧)已在[外部代码]上停止,如果选择“显示外部代码”,则可以详细查看这些代码。

    Parallel Stacks Window

    在此示例中,从左到右会看到托管代码的以下信息:

    • 主线程(左侧)已在 Thread.Start 处停止,停止点由线程标记图标 Thread Marker 标识。
    • 两个线程已进入 ServerClass.InstanceMethod,其中一个线程是当前线程(黄色箭头),另一个线程已停止在 Thread.Sleep 中。
    • 新线程(右侧)也已启动,但是停止在 ThreadHelper.ThreadStart 上。
  2. 若要在列表视图中查看线程,请选择“调试”>“Windows”>“线程”。

    Screenshot of the Threads Window.

    在此视图中,可以轻松看到线程 20272 是主线程,当前位于外部代码中,特别是 System.Console.dll

    注意

    有关使用“线程”窗口的详细信息,请参阅演练:调试多线程应用程序

  3. 右键单击“并行堆栈”或“线程”窗口中的条目,查看快捷菜单上的可用选项。

    可以从这些右键单击菜单中执行各种操作。 对于本教程,在“并行监视”窗口(后续各节)中详细探索这些详细信息

对变量设置监视

  1. 通过选择“调试” >“窗口” >“并行监视” >“并行监视 1” ,打开“并行监视”窗口 。

  2. 选择 <Add Watch> 文本所在的单元格(或第 4 列中的空标头单元格),输入 data

    窗口中会显示每个线程的数据变量的值。

  3. 选择 <Add Watch> 文本所在的单元格(或第 5 列中的空标头单元格),输入 count

    窗口中会显示每个线程的 count 变量的值。 如果看不到这么多的信息,请尝试按 F11 几次以继续在调试器中执行线程。

    Parallel Watch Window

  4. 右键单击窗口中的某一行以查看可用选项。

标记线程和取消标记线程

可以通过标记线程来追踪重要的线程,并忽略其它线程。

  1. 在“并行监视” 窗口中,按住 Shift 键并选择多行。

  2. 右键单击并选择“标记” 。

    系统会标记所有选定的线程。 现在,可以进行筛选以仅显示标记的线程。

  3. 在“并行监视”窗口中,选择“仅显示标记的线程”按钮 Show Flagged Threads

    列表中仅显示标记的线程。

    提示

    在标记一些线程后,可以右键单击代码编辑器中的代码行,然后选择“将标记的线程运行到光标处” 。 请确保选择所有已标记的线程将达到的代码。 Visual Studio 将在选择的代码行处暂停线程,这样就可以通过冻结和解冻线程更容易地控制执行顺序。

  4. 再次选择“仅显示标记的线程” 按钮,以切换回“显示全部线程” 模式。

  5. 若要取消标记线程,请在“并行监视” 窗口右键单击一个或多个已标记线程,然后选择“取消标记” 。

冻结和解冻线程执行

提示

可以通过冻结和解冻(暂停和恢复)线程来控制线程执行工作的顺序。 这有助于解决并发问题,例如死锁和争用条件。

  1. 在“并行监视” 窗口中,在选中所有行的情况下,右键单击并选择“冻结” 。

    在第二个列中,每个行出现一个暂停图标。 暂停图标指示该线程已冻结。

  2. 仅选择一行,取消选中其他行。

  3. 右键单击某行并选择“解冻” 。

    暂停图标在此行上消失,表明线程已不再被冻结。

  4. 切换到代码编辑器,按 F11。 仅运行未冻结的线程。

    应用可能还实例化某些新线程。 任何新线程均处于未标记状态,不会被冻结。

使用条件断点跟踪单个线程

可以在调试器中对单线程的执行情况进行跟踪。 一种方法是冻结不感兴趣的线程。 在某些情况下,可能需要在不冻结其它线程的情况下跟踪单个线程,例如重现特定 Bug。 若要在不冻结其他线程的情况下跟踪某个线程,必须避免在不感兴趣的线程上中断。 可通过设置条件断点来执行此任务。

可以根据不同的条件(例如,线程名称或线程 ID)来设置断点。 对于已知对每个线程唯一的数据,设置该条件会有所帮助。 当你对某些特定数据值比对任何特定线程更感兴趣时,这种方法在调试过程中很常见。

  1. 右键单击先前创建的断点,然后选择“条件” 。

  2. 在“断点设置” 窗口中,输入 data == 5 作为条件表达式。

    Conditional Breakpoint

    提示

    如果对特定的线程更感兴趣,则请使用线程名称或线程 ID 作为条件。 若要在“断点设置” 窗口中执行此操作,请选择“筛选器” 而不是“条件表达式” ,并按照筛选器提示操作。 可能需要在应用代码中指定线程名称,因为线程 ID 在重启调试器时会更改。

  3. 关闭“断点设置” 窗口。

  4. 选择重启 Restart App 按钮以重启调试会话。

    在数据变量的值为 5 的线程中,中断代码执行。 请在“并行监视” 窗口中,寻找表示当前调试器上下文的黄色箭头。

  5. 现在,可以逐过程执行代码 (F10) 和单步执行代码 (F11),并跟踪单个线程的执行情况。

    只要断点条件对于线程是唯一的,并且调试器不会在其他线程上命中其他任何断点(你可能需要禁用它们),你就可以逐过程执行代码和单步执行代码,而无需切换到其他线程。

    注意

    调试器前进时,所有线程都将运行。 但是,调试器不会中断其他线程上的代码,除非其中一个线程命中断点。