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

Visual Studio 提供了多个工具和用户界面元素来帮助您调试多线程应用程序。 本教程介绍如何使用线程标记、 Parallel Stacks 窗口、 Parallel Watch 窗口、条件断点和筛选器断点。 完成本教程后,您将熟悉用于调试多线程应用程序的 Visual Studio 功能。

以下两篇文章提供了有关使用其他多线程调试工具的其他信息:

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

创建多线程应用程序项目

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

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

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

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

    应用语言和平台筛选条件后,选择适用于 .NET 或 C++ 的 Console App (控制台应用程序 ) 模板,然后选择 Next (下一步)。

    注释

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

    Configure your new project (配置新项目) 窗口中,在 Project name (项目名称) 框中键入或输入 MyThreadWalkthroughApp。 然后,选择 Next (下一步 ) 或 Create (创建),以可用选项为准。

    对于 .NET Core 或 .NET 5+ 项目,请选择建议的目标框架或 .NET 8,然后选择 Create(创建)。

    会出现一个新的控制台项目。 创建项目后,将显示源文件。 根据您选择的语言,源文件可能称为 Program.csMyThreadWalkthroughApp.cppModule1.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)在 Solution Explorer(右窗格)中,右键单击项目节点,选择 Properties (属性)。 在 Application (应用程序 ) 选项卡下,将 Startup 对象 更改为 Simple (简单)。

调试多线程应用

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

    Thread.Sleep(3000);
    Console.WriteLine();
    
  2. 左键单击 or for C++ std::this_thread::sleep_for 语句的Thread.Sleep左侧装订线以插入新的断点。

    在装订线中,红色圆圈表示在此位置设置了断点。

  3. Debug (调试 ) 菜单上,选择 Start Debugging ( F5) (启动调试) (F5)。

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

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

发现线程标记

  1. 在 Debug Toolbar 中,选择 Show Threads in Source 按钮 Show Threads in Source

  2. F11 两次可前进调试器。

  3. 查看窗口左侧的槽。 在此行中,请注意一个 线程标记 图标 Thread Marker ,它类似于两条扭曲的线程。 线程标记指示线程在此位置停止。

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

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

    数据提示中线程 ID 的屏幕截图。

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

查看线程位置

Parallel Stacks (并行堆栈) 窗口中,您可以在 Threads (线程) 视图和 (对于基于任务的编程) Tasks (任务) 视图之间切换,并且可以查看每个线程的调用堆栈信息。 在这个应用程序中,我们可以使用 Threads 视图。

  1. 通过选择 Debug WindowsParallel Stacks调试>Windows> 并行堆栈) 打开 Parallel Stacks (并行堆栈) 窗口。 此时会看到如下所示的内容。 确切的信息可能会有所不同,具体取决于每个线程的当前位置、硬件和编程语言。

    Parallel Stacks (并行堆栈) 窗口的屏幕截图。

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

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

    Parallel Stacks 窗口

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

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

    Threads 窗口的屏幕截图。

    在此视图中,您可以很容易地看到线程 20272 是主线程,当前位于外部代码中,特别是 System.Console.dll中。

    注释

    有关使用 Threads 窗口的更多信息,请参见 演练:调试多线程应用程序

  3. 右键单击 Parallel StacksThreads 窗口中的项,查看快捷菜单上的可用选项。

    您可以从这些右键单击菜单中执行各种作。 在本教程中,您将在 Parallel Watch (Parallel Watch ) 窗口(下一部分)中探索更多详细信息。

对变量设置监视

  1. 通过选择 Debug>Windows>Parallel WatchParallel Watch> 1 打开 Parallel Watch 窗口。

  2. 选择您看到 <Add Watch> 文本的单元格(或第四列中的空标题单元格),然后输入 data

    每个线程的 data 变量的值将显示在窗口中。

  3. 选择您看到 <Add Watch> 文本的单元格(或第五列中的空标题单元格),然后输入 count

    每个线程的变量值 count 将显示在窗口中。 如果您还没有看到这么多信息,请尝试按 F11 几次,以推进调试器中线程的执行。

    并行监视窗口

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

标记和取消标记线程

您可以标记线程以跟踪重要线程并忽略其他线程。

  1. Parallel Watch 窗口中,按住 Shift 键并选择多行。

  2. 右键单击并选择 Flag (标志)。

    将标记所有选定的线程。 现在,您可以进行筛选以仅显示已标记的线程。

  3. Parallel Watch 窗口中,选择 Show Only Flagged Threads 按钮 Show Flagged Threads

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

    小窍门

    标记某些线程后,可以在代码编辑器中右键单击一行代码,然后选择 Run Flagged Threads to Cursor。 确保选择所有标记的线程都将访问的代码。 Visual Studio 将暂停所选代码行上的线程,从而更容易通过 冻结和解冻线程来控制执行顺序。

  4. 再次选择 Show Only Flagged Threads 按钮,切换回 Show All Threads 模式。

  5. 要取消标记线程,请右键单击 Parallel Watch (并行监视 ) 窗口中的一个或多个已标记的线程,然后选择 Unflag (取消标记)。

冻结和解冻线程执行

小窍门

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

  1. Parallel Watch 窗口中,选中所有行后,右键单击并选择 Freeze

    在第二列中,每行都会显示一个暂停图标。 暂停图标表示线程已冻结。

  2. 通过仅选择一行来取消选择所有其他行。

  3. 右键单击一行,然后选择 Thaw

    暂停图标在此行上消失,这表示线程不再冻结。

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

    该应用程序还可能会实例化一些新线程。 任何新线程都不会被标记,也不会被冻结。

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

在调试器中跟踪单个线程的执行情况可能会有所帮助。 一种方法是冻结您不感兴趣的线程。 在某些情况下,您可能需要关注单个线程而不冻结其他线程,例如,重现特定 bug。 要在不冻结其他线程的情况下跟踪线程,必须避免中断除您感兴趣的线程之外的代码。 您可以通过设置 条件断点来完成此任务。

您可以根据不同的条件设置断点,例如线程名称或线程 ID。 对已知每个线程唯一的数据设置条件可能会很有帮助。 在调试期间,当您对某些特定数据值比对任何特定线程更感兴趣时,此方法很常见。

  1. 右键单击您之前创建的断点,然后选择 Conditions

  2. Breakpoint Settings (断点设置 ) 窗口中,输入 data == 5 条件表达式。

    条件断点

    小窍门

    如果您对特定线程更感兴趣,请为条件使用线程名称或线程 ID。 要在 Breakpoint Settings (断点设置 ) 窗口中执行此作,请选择 Filter (筛选 ) 而不是 Conditional expression (条件表达式),然后按照筛选提示进行作。 您可能希望在应用程序代码中 命名线程 ,因为当您重新启动调试器时,线程 ID 会发生变化。

  3. 关闭 Breakpoint Settings 窗口。

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

    您中断了 data 变量值为 5 的线程上的代码。 在 Parallel Watch (并行监视 ) 窗口中,查找指示当前调试器上下文的黄色箭头。

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

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

    注释

    当您推进调试器时,所有线程都将运行。 但是,调试器不会中断其他线程上的代码,除非其他线程之一遇到断点。