开始调试多线程应用程序(C#、Visual Basic、C++)
Visual Studio 提供多种工具和用户界面元素,用于调试多线程应用程序。 本教程演示如何使用线程标记、“并行堆栈” 窗口、“并行监视” 窗口、条件断点、筛选器断点。 完成本教程可使你熟悉用于调试多线程应用程序的 Visual Studio 功能。
下面两篇文章额外介绍了如何使用其他多线程调试工具:
若要使用“调试位置” 工具栏和“线程” 窗口,请参阅演练:调试多线程应用程序。
有关使用 Task(托管代码)和并发运行时 (C++) 的示例,请参阅演练:调试并行应用程序。 有关适用于大多数多线程应用程序类型的常规调试技巧,请阅读该文章和本文章。
第一步是创建多线程应用程序项目。
创建一个多线程应用项目
打开 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 。
删除源文件中显示的代码,并将其替换为以下更新的代码。 为代码配置选择适当的代码片段。
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."); } }
在“文件” 菜单上,单击“全部保存” 。
(仅适用于 Visual Basic)在“解决方案资源管理器”(右窗格)中,右键单击项目节点,然后选择“属性” 。 在“应用程序” 选项卡下,将“启动对象” 更改为“简单” 。
调试多线程应用
在源代码编辑器中,查找以下代码片段:
在
Thread.Sleep
语句或std::this_thread::sleep_for
语句(针对 C++)的左滚动条槽中单击左键,以插入新的断点。在滚动条槽中,红色圆圈表示已在该位置设置了一个断点。
在“调试” 菜单上,单击“开始调试(F5)” 。
Visual Studio 将生成该解决方案,应用在附加了调试器的情况下开始运行,然后在断点处停止。
在源代码编辑器中,找到包含断点的行。
发现线程标记
在调试工具栏中,单击“在源中显示线程”按钮 。
按 F11 两次以完成调试器的下一步。
查看窗口左侧的滚动条槽。 在此行中,注意“线程标记”图标 ,类似于一条双绞线。 线程标记指示线程在此位置停止。
线程标记可以被断点部分隐藏。
将指针悬停在线程标记上。 此时会出现一个数据提示,告知你每个已停止线程的名称和线程 ID 号。 在这种情况下,名称可能是
<noname>
。选择线程标记,以查看快捷菜单上的可用选项。
查看线程位置
在“并行堆栈” 窗口中,可以在“线程”视图和“任务”视图(适用于基于任务的编程)之间进行切换,并且可以查看每个线程的调用堆栈信息。 在此应用中,我们可以使用“线程”视图。
通过选择“调试”>“窗口”>“并行堆栈” ,打开“并行堆栈” 窗口。 此时会看到如下所示的内容。 确切信息可能因每个线程的当前位置、硬件和编程语言而异。
在此示例中,从左到右会看到托管代码的以下信息:
- 当前线程(黄色箭头)已进入
ServerClass.InstanceMethod
。 可以通过将鼠标悬停在ServerClass.InstanceMethod
上来查看线程的线程 ID 和堆栈帧。 - 线程 31724 正在等待线程 20272 拥有的锁。
- 主线程(左侧)已在[外部代码]上停止,如果选择“显示外部代码”,则可以详细查看这些代码。
在此示例中,从左到右会看到托管代码的以下信息:
- 主线程(左侧)已在
Thread.Start
处停止,停止点由线程标记图标 标识。 - 两个线程已进入
ServerClass.InstanceMethod
,其中一个线程是当前线程(黄色箭头),另一个线程已停止在Thread.Sleep
中。 - 新线程(右侧)也已启动,但是停止在
ThreadHelper.ThreadStart
上。
- 当前线程(黄色箭头)已进入
若要在列表视图中查看线程,请选择“调试”>“Windows”>“线程”。
在此视图中,可以轻松看到线程 20272 是主线程,当前位于外部代码中,特别是 System.Console.dll。
注意
有关使用“线程”窗口的详细信息,请参阅演练:调试多线程应用程序。
右键单击“并行堆栈”或“线程”窗口中的条目,查看快捷菜单上的可用选项。
可以从这些右键单击菜单中执行各种操作。 对于本教程,在“并行监视”窗口(后续各节)中详细探索这些详细信息。
对变量设置监视
通过选择“调试” >“窗口” >“并行监视” >“并行监视 1” ,打开“并行监视”窗口 。
选择
<Add Watch>
文本所在的单元格(或第 4 列中的空标头单元格),输入data
。窗口中会显示每个线程的数据变量的值。
选择
<Add Watch>
文本所在的单元格(或第 5 列中的空标头单元格),输入count
。窗口中会显示每个线程的
count
变量的值。 如果看不到这么多的信息,请尝试按 F11 几次以继续在调试器中执行线程。右键单击窗口中的某一行以查看可用选项。
标记线程和取消标记线程
可以通过标记线程来追踪重要的线程,并忽略其它线程。
在“并行监视” 窗口中,按住 Shift 键并选择多行。
右键单击并选择“标记” 。
系统会标记所有选定的线程。 现在,可以进行筛选以仅显示标记的线程。
在“并行监视”窗口中,选择“仅显示标记的线程”按钮 。
列表中仅显示标记的线程。
提示
在标记一些线程后,可以右键单击代码编辑器中的代码行,然后选择“将标记的线程运行到光标处” 。 请确保选择所有已标记的线程将达到的代码。 Visual Studio 将在选择的代码行处暂停线程,这样就可以通过冻结和解冻线程更容易地控制执行顺序。
再次选择“仅显示标记的线程” 按钮,以切换回“显示全部线程” 模式。
若要取消标记线程,请在“并行监视” 窗口右键单击一个或多个已标记线程,然后选择“取消标记” 。
冻结和解冻线程执行
提示
可以通过冻结和解冻(暂停和恢复)线程来控制线程执行工作的顺序。 这有助于解决并发问题,例如死锁和争用条件。
在“并行监视” 窗口中,在选中所有行的情况下,右键单击并选择“冻结” 。
在第二个列中,每个行出现一个暂停图标。 暂停图标指示该线程已冻结。
仅选择一行,取消选中其他行。
右键单击某行并选择“解冻” 。
暂停图标在此行上消失,表明线程已不再被冻结。
切换到代码编辑器,按 F11。 仅运行未冻结的线程。
应用可能还实例化某些新线程。 任何新线程均处于未标记状态,不会被冻结。
使用条件断点跟踪单个线程
可以在调试器中对单线程的执行情况进行跟踪。 一种方法是冻结不感兴趣的线程。 在某些情况下,可能需要在不冻结其它线程的情况下跟踪单个线程,例如重现特定 Bug。 若要在不冻结其他线程的情况下跟踪某个线程,必须避免在不感兴趣的线程上中断。 可通过设置条件断点来执行此任务。
可以根据不同的条件(例如,线程名称或线程 ID)来设置断点。 对于已知对每个线程唯一的数据,设置该条件会有所帮助。 当你对某些特定数据值比对任何特定线程更感兴趣时,这种方法在调试过程中很常见。
右键单击先前创建的断点,然后选择“条件” 。
在“断点设置” 窗口中,输入
data == 5
作为条件表达式。提示
如果对特定的线程更感兴趣,则请使用线程名称或线程 ID 作为条件。 若要在“断点设置” 窗口中执行此操作,请选择“筛选器” 而不是“条件表达式” ,并按照筛选器提示操作。 可能需要在应用代码中指定线程名称,因为线程 ID 在重启调试程序时会更改。
关闭“断点设置” 窗口。
选择重启 按钮以重启调试会话。
在数据变量的值为 5 的线程中,中断代码执行。 请在“并行监视” 窗口中,寻找表示当前调试器上下文的黄色箭头。
现在,可以逐过程执行代码 (F10) 和单步执行代码 (F11),并跟踪单个线程的执行情况。
只要断点条件对于线程是唯一的,并且调试器不会在其他线程上命中其他任何断点(你可能需要禁用它们),你就可以逐过程执行代码和单步执行代码,而无需切换到其他线程。
注意
调试器前进时,所有线程都将运行。 但是,调试器不会中断其他线程上的代码,除非其中一个线程命中断点。