使用Visual Studio调试多线程应用程序
使用Visual Studio调试多线程应用程序
本文将详细介绍如何使用Visual Studio调试多线程应用程序。通过本文,你将学习到如何使用线程标记、并行堆栈窗口、并行监视窗口、条件断点等工具,以及如何创建和调试多线程应用项目。本文提供了C#、Visual Basic和C++三种语言的代码示例,适合有一定编程基础的技术人员阅读。
创建一个多线程应用项目
- 打开Visual Studio并创建一个新项目。如果“开始”窗口未打开,请选择“文件”>“启动窗口”。
- 在“开始”窗口上,选择“创建新项目”。
- 在“创建新项目”窗口的搜索框中输入或键入“控制台”。接下来,从“语言”列表中选择“C#”、“C++”或“Visual Basic”,然后从“平台”列表中选择“Windows”。
- 应用语言和平台筛选器之后,对.NET或C++选择“控制台应用”模板,然后选择“下一步”。
- 在“配置新项目”窗口中,在“项目名称”框中键入或输入MyThreadWalkthroughApp。然后,选择“下一步”或“创建”(视具体提供的选项而定)。
- 对于.NET Core或.NET 5+项目,选择建议的目标框架或.NET 8,然后选择“创建”。
新的控制台项目随即显示。创建该项目后,将显示源文件。根据所选语言,源文件名称可能是Program.cs、MyThreadWalkthroughApp.cpp或Module1.vb。
- 删除源文件中显示的代码,并将其替换为以下更新的代码。为代码配置选择适当的代码片段。
C#代码示例
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代码示例
Imports System.Threading
Public Class ServerClass
' The method that will be called when the thread is started.
Public count = 0
Public Sub InstanceMethod()
Console.WriteLine(
"ServerClass.InstanceMethod is running on another thread.")
Dim data = count + 1
' 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)
End Sub
End Class
Public Class Simple
Public Shared Sub Main()
Dim ts As New ThreadStarter
For index = 1 To 10
ts.CreateThreads()
Next
End Sub
End Class
Public Class ThreadStarter
Public Sub CreateThreads()
Dim serverObject As New ServerClass()
' Create the thread object, passing in the
' serverObject.InstanceMethod method using a
' ThreadStart delegate.
Dim InstanceCaller As New Thread(AddressOf serverObject.InstanceMethod)
' Start the thread.
InstanceCaller.Start()
Console.WriteLine("The Main() thread calls this after " _
+ "starting the new InstanceCaller thread.")
End Sub
End Class
C++代码示例
#include <thread>
#include <iostream>
#include <vector>
#include <string>
int count = 0;
void doSomeWork() {
std::cout << "The doSomeWork function is running on another thread." << std::endl;
int data = count++;
// Pause for a moment to provide a delay to make
// threads more apparent.
std::this_thread::sleep_for(std::chrono::seconds(3));
std::string str = std::to_string(data);
std::cout << "The function called by the worker thread has ended. " + str << std::endl;
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(doSomeWork));
std::cout << "The Main() thread calls this after starting the new thread" << std::endl;
}
for (auto& thread : threads) {
thread.join();
}
return 0;
}
在“文件”菜单上,单击“全部保存”。
(仅适用于Visual Basic)在“解决方案资源管理器”(右窗格)中,右键单击项目节点,然后选择“属性”。在“应用程序”选项卡下,将“启动对象”更改为“简单”。
调试多线程应用
- 在源代码编辑器中,查找以下代码片段:
Thread.Sleep(3000);
Console.WriteLine();
Thread.Sleep(3000)
Console.WriteLine()
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "The function called by the worker thread has ended." << std::endl;
在Thread.Sleep语句或std::this_thread::sleep_for语句(针对C++)的左滚动条槽中单击左键,以插入新的断点。在滚动条槽中,红色圆圈表示已在该位置设置了一个断点。
在“调试”菜单上,单击“开始调试(F5)”。Visual Studio将生成该解决方案,应用在附加了调试器的情况下开始运行,然后在断点处停止。
在源代码编辑器中,找到包含断点的行。
发现线程标记
在调试工具栏中,单击“在源中显示线程”按钮。
按F11两次以完成调试器的下一步。
查看窗口左侧的滚动条槽。在此行中,注意“线程标记”图标,类似于一条双绞线。线程标记指示线程在此位置停止。线程标记可以被断点部分隐藏。
将指针悬停在线程标记上。此时会出现一个数据提示,告知你每个已停止线程的名称和线程ID号。在这种情况下,名称可能是
。 选择线程标记,以查看快捷菜单上的可用选项。
查看线程位置
在“并行堆栈”窗口中,可以在“线程”视图和“任务”视图(适用于基于任务的编程)之间进行切换,并且可以查看每个线程的调用堆栈信息。在此应用中,我们可以使用“线程”视图。
- 通过选择“调试”>“窗口”>“并行堆栈”,打开“并行堆栈”窗口。此时会看到如下所示的内容。确切信息可能因每个线程的当前位置、硬件和编程语言而异。
在此示例中,从左到右会看到托管代码的以下信息:
- 当前线程(黄色箭头)已进入ServerClass.InstanceMethod。可以通过将鼠标悬停在ServerClass.InstanceMethod上来查看线程的线程ID和堆栈帧。
- 线程31724正在等待线程20272拥有的锁。
- 主线程(左侧)已在[外部代码]上停止,如果选择“显示外部代码”,则可以详细查看这些代码。
在此示例中,从左到右会看到托管代码的以下信息:
- 主线程(左侧)已在Thread.Start处停止,停止点由线程标记图标标识。
- 两个线程已进入ServerClass.InstanceMethod,其中一个线程是当前线程(黄色箭头),另一个线程已停止在Thread.Sleep中。
- 新线程(右侧)也已启动,但是停止在ThreadHelper.ThreadStart上。
- 若要在线列表视图中查看线程,请选择“调试”>“Windows”>“线程”。
在此视图中,可以轻松看到线程20272是主线程,当前位于外部代码中,特别是System.Console.dll。
注意:有关使用“线程”窗口的详细信息,请参阅演练:调试多线程应用程序。
右键单击“并行堆栈”或“线程”窗口中的条目,查看快捷菜单上的可用选项。
可以从这些右键单击菜单中执行各种操作。对于本教程,在“并行监视”窗口(后续各节)中详细探索这些详细信息。
对变量设置监视
通过选择“调试” >“窗口” >“并行监视” >“并行监视 1”,打开“并行监视”窗口。
选择
文本所在的单元格(或第4列中的空标头单元格),输入data。
窗口中会显示每个线程的数据变量的值。
- 选择
文本所在的单元格(或第5列中的空标头单元格),输入count。
窗口中会显示每个线程的count变量的值。如果看不到这么多的信息,请尝试按F11几次以继续在调试器中执行线程。
- 右键单击窗口中的某一行以查看可用选项。
标记线程和取消标记线程
可以通过标记线程来追踪重要的线程,并忽略其它线程。
在“并行监视”窗口中,按住Shift键并选择多行。
右键单击并选择“标记”。
系统会标记所有选定的线程。现在,可以进行筛选以仅显示标记的线程。
- 在“并行监视”窗口中,选择“仅显示标记的线程”按钮。
列表中仅显示标记的线程。
提示:在标记一些线程后,可以右键单击代码编辑器中的代码行,然后选择“将标记的线程运行到光标处”。请确保选择所有已标记的线程将达到的代码。Visual Studio将在选择的代码行处暂停线程,这样就可以通过冻结和解冻线程更容易地控制执行顺序。
再次选择“仅显示标记的线程”按钮,以切换回“显示全部线程”模式。
若要取消标记线程,请在“并行监视”窗口右键单击一个或多个已标记线程,然后选择“取消标记”。
冻结和解冻线程执行
提示:可以通过冻结和解冻(暂停和恢复)线程来控制线程执行工作的顺序。这有助于解决并发问题,例如死锁和争用条件。
- 在“并行监视”窗口中,在选中所有行的情况下,右键单击并选择“冻结”。
在第二个列中,每个行出现一个暂停图标。暂停图标指示该线程已冻结。
仅选择一行,取消选中其他行。
右键单击某行并选择“解冻”。
暂停图标在此行上消失,表明线程已不再被冻结。
- 切换到代码编辑器,按F11。仅运行未冻结的线程。
应用可能还实例化某些新线程。任何新线程均处于未标记状态,不会被冻结。
使用条件断点跟踪单个线程
可以在调试器中对单线程的执行情况进行跟踪。一种方法是冻结不感兴趣的线程。在某些情况下,可能需要在不冻结其他线程的情况下跟踪单个线程,例如重现特定Bug。要在一个不冻结其他线程的情况下跟踪某个线程,必须避免在不感兴趣的线程上中断。可以通过设置条件断点来执行此任务。
可以根据不同的条件(例如,线程名称或线程ID)来设置断点。对于已知对每个线程唯一的数据,设置该条件会有所帮助。当你对某些特定数据值比对任何特定线程更感兴趣时,这种方法在调试过程中很常见。
右键单击先前创建的断点,然后选择“条件”。
在“断点设置”窗口中,输入data == 5作为条件表达式。
提示:如果对特定的线程更感兴趣,则请使用线程名称或线程ID作为条件。要在“断点设置”窗口中执行此操作,请选择“筛选器”而不是“条件表达式”,并按照筛选器提示操作。可能需要在应用代码中指定线程名称,因为线程ID在重启调试程序时会更改。
关闭“断点设置”窗口。
选择重启按钮以重启调试会话。
在数据变量的值为5的线程中,中断代码执行。请在“并行监视”窗口中,寻找表示当前调试器上下文的黄色箭头。
- 现在,可以逐过程执行代码(F10)和单步执行代码(F11),并跟踪单个线程的执行情况。
只要断点条件对于线程是唯一的,并且调试器不会在其他线程上命中其他任何断点(你可能需要禁用它们),你就可以逐过程执行代码和单步执行代码,而无需切换到其他线程。
注意:调试器前进时,所有线程都将运行。但是,调试器不会中断其他线程上的代码,除非其中一个线程命中断点。