本演练演示如何使用 并行任务 和 并行堆栈 窗口调试并行应用程序。 这些窗口可帮助你了解和验证使用 任务并行库(TPL) 或 并发运行时的代码的运行时行为。 本演练提供具有内置断点的示例代码。 代码中断后,演练演示如何使用 并行任务 和 并行堆栈 窗口对其进行检查。
本演练介绍以下任务:
如何在一个视图中查看所有线程的调用堆栈。
如何查看在应用程序中创建的实例列表
System.Threading.Tasks.Task
。如何查看任务的实际调用堆栈而不是线程。
如何从 “并行任务 ”和 “并行堆栈” 窗口导航到代码。
窗口如何通过组合、动态调整和其他相关功能来应对规模变化。
先决条件
本演练假定“ 仅我的代码 ”已启用(默认情况下在较新版本的 Visual Studio 中启用)。 在“工具”菜单上,选择“选项”,展开“调试”节点,选择“常规”,然后选择“仅启用我的代码”(仅托管)。 如果未设置此功能,仍可使用本演练,但结果可能与插图不同。
C# 示例
如果使用 C# 示例,本演练还假定外部代码已隐藏。 若要切换是否显示外部代码,请右键单击“调用堆栈”窗口的“名称”表标头,然后选择或清除“显示外部代码”。 如果未设置此功能,仍可使用本演练,但结果可能与插图不同。
C++示例
如果使用C++示例,可以忽略本文中对外部代码的引用。 外部代码仅适用于 C# 示例。
图示
本文中的插图记录在运行 C# 示例的四核计算机上。 尽管可以使用其他配置来完成本演练,但插图可能与计算机上显示的内容不同。
创建示例项目
本演练中的示例代码是用于不执行任何操作的应用程序。 本练习的目的是了解如何使用工具窗口调试并行应用程序。
打开 Visual Studio 并创建新项目。
如果启动窗口未打开,请选择 “文件”>“开始”窗口。
在“开始”窗口中,选择“ 新建项目”。
在“开始”窗口上,选择创建新项目。
在 “创建新项目 ”窗口中,在搜索框中输入或键入 控制台 。 接下来,从语言列表中选择 C#、 C++ 或 Visual Basic ,然后从平台列表中选择 Windows 。
应用语言和平台筛选器后,选择适用于 .NET Core 的 控制台应用 或C++,然后选择“ 下一步”。
注释
如果未看到正确的模板,请转到 “工具>获取工具和功能...”,这将打开 Visual Studio 安装程序。 选择具有C++工作负荷的 .NET 桌面开发 或 桌面开发 ,然后选择 “修改”。
在 “配置新项目 ”窗口中,键入名称或使用 “项目名称 ”框中的默认名称。 然后选择 “下一步 ”或 “创建”,无论哪个选项可用。
对于 .NET Core,请选择建议的目标框架或 .NET 8,然后选择 创建。
会出现一个新的控制台项目。 创建项目后,将显示源文件。
打开项目中的.cpp、.cs或.vb代码文件。 删除其内容以创建空代码文件。
将所选语言的以下代码粘贴到空代码文件中。
using System; using System.Threading; using System.Threading.Tasks; using System.Diagnostics; class S { static void Main() { pcount = Environment.ProcessorCount; Console.WriteLine("Proc count = " + pcount); ThreadPool.SetMinThreads(4, -1); ThreadPool.SetMaxThreads(4, -1); t1 = new Task(A, 1); t2 = new Task(A, 2); t3 = new Task(A, 3); t4 = new Task(A, 4); Console.WriteLine("Starting t1 " + t1.Id.ToString()); t1.Start(); Console.WriteLine("Starting t2 " + t2.Id.ToString()); t2.Start(); Console.WriteLine("Starting t3 " + t3.Id.ToString()); t3.Start(); Console.WriteLine("Starting t4 " + t4.Id.ToString()); t4.Start(); Console.ReadLine(); } static void A(object o) { B(o); } static void B(object o) { C(o); } static void C(object o) { int temp = (int)o; Interlocked.Increment(ref aa); while (aa < 4) { ; } if (temp == 1) { // BP1 - all tasks in C Debugger.Break(); waitFor1 = false; } else { while (waitFor1) { ; } } switch (temp) { case 1: D(o); break; case 2: F(o); break; case 3: case 4: I(o); break; default: Debug.Assert(false, "fool"); break; } } static void D(object o) { E(o); } static void E(object o) { // break here at the same time as H and K while (bb < 2) { ; } //BP2 - 1 in E, 2 in H, 3 in J, 4 in K Debugger.Break(); Interlocked.Increment(ref bb); //after L(o); } static void F(object o) { G(o); } static void G(object o) { H(o); } static void H(object o) { // break here at the same time as E and K Interlocked.Increment(ref bb); Monitor.Enter(mylock); while (bb < 3) { ; } Monitor.Exit(mylock); //after L(o); } static void I(object o) { J(o); } static void J(object o) { int temp2 = (int)o; switch (temp2) { case 3: t4.Wait(); break; case 4: K(o); break; default: Debug.Assert(false, "fool2"); break; } } static void K(object o) { // break here at the same time as E and H Interlocked.Increment(ref bb); Monitor.Enter(mylock); while (bb < 3) { ; } Monitor.Exit(mylock); //after L(o); } static void L(object oo) { int temp3 = (int)oo; switch (temp3) { case 1: M(oo); break; case 2: N(oo); break; case 4: O(oo); break; default: Debug.Assert(false, "fool3"); break; } } static void M(object o) { // breaks here at the same time as N and Q Interlocked.Increment(ref cc); while (cc < 3) { ; } //BP3 - 1 in M, 2 in N, 3 still in J, 4 in O, 5 in Q Debugger.Break(); Interlocked.Increment(ref cc); while (true) Thread.Sleep(500); // for ever } static void N(object o) { // breaks here at the same time as M and Q Interlocked.Increment(ref cc); while (cc < 4) { ; } R(o); } static void O(object o) { Task t5 = Task.Factory.StartNew(P, TaskCreationOptions.AttachedToParent); t5.Wait(); R(o); } static void P() { Console.WriteLine("t5 runs " + Task.CurrentId.ToString()); Q(); } static void Q() { // breaks here at the same time as N and M Interlocked.Increment(ref cc); while (cc < 4) { ; } // task 5 dies here freeing task 4 (its parent) Console.WriteLine("t5 dies " + Task.CurrentId.ToString()); waitFor5 = false; } static void R(object o) { if ((int)o == 2) { //wait for task5 to die while (waitFor5) { ;} int i; //spin up all procs for (i = 0; i < pcount - 4; i++) { Task t = Task.Factory.StartNew(() => { while (true);}); Console.WriteLine("Started task " + t.Id.ToString()); } Task.Factory.StartNew(T, i + 1 + 5, TaskCreationOptions.AttachedToParent); //scheduled Task.Factory.StartNew(T, i + 2 + 5, TaskCreationOptions.AttachedToParent); //scheduled Task.Factory.StartNew(T, i + 3 + 5, TaskCreationOptions.AttachedToParent); //scheduled Task.Factory.StartNew(T, i + 4 + 5, TaskCreationOptions.AttachedToParent); //scheduled Task.Factory.StartNew(T, (i + 5 + 5).ToString(), TaskCreationOptions.AttachedToParent); //scheduled //BP4 - 1 in M, 2 in R, 3 in J, 4 in R, 5 died Debugger.Break(); } else { Debug.Assert((int)o == 4); t3.Wait(); } } static void T(object o) { Console.WriteLine("Scheduled run " + Task.CurrentId.ToString()); } static Task t1, t2, t3, t4; static int aa = 0; static int bb = 0; static int cc = 0; static bool waitFor1 = true; static bool waitFor5 = true; static int pcount; static S mylock = new S(); }
更新代码文件后,保存更改并生成解决方案。
在“文件”菜单上,单击“全部保存”。
在“生成”菜单中,选择“重新生成解决方案”。
请注意,有四个 Debugger.Break
调用(DebugBreak
在C++示例中)。 因此,无需插入断点。 只需运行应用程序,它就会导致调试器中断最多四次。
使用“并行堆栈”窗口:“线程”视图
若要开始,请在 “调试 ”菜单上,选择“ 开始调试”。 请等待直到第一个断点被触发。
查看单个线程的调用堆栈
在 “调试 ”菜单上,指向 Windows ,然后选择“ 线程”。 将线程窗口停靠在 Visual Studio 底部。
在 “调试” 菜单上,指向 Windows ,然后选择“ 调用堆栈”。 将 “调用堆栈” 窗口停靠在 Visual Studio 的底部。
双击“ 线程 ”窗口中的线程使其保持最新状态。 当前的线程用黄色箭头表示。 更改当前线程时,其调用堆栈将显示在 “调用堆栈” 窗口中。
检查“并行堆栈”窗口
在 “调试” 菜单上,指向 Windows ,然后选择 “并行堆栈”。 请确保在左上角的框中选择线程。
通过使用 “并行堆栈” 窗口,可以在一个视图中同时查看多个调用堆栈。 下图显示了“调用堆栈”窗口上方的“并行堆栈”窗口。
主线程的调用堆栈显示在一个框中,另外四个线程的调用堆栈将分组在另一个框中。 四个线程组合在一起,因为它们的堆栈帧共享相同的方法上下文;也就是说,它们采用相同的方法: A
、 B
和 C
。 若要查看共享同一框的线程的线程 ID 和名称,请将鼠标悬停在具有标头([#] 线程)的框上。 当前线程以粗体显示。
黄色箭头指示当前线程的活动堆栈帧。
通过右键单击“调用堆栈”窗口中,可以设置堆栈帧(模块名称、参数类型、参数名称、参数值、行号和字节偏移量)要显示的详细信息。
框周围的蓝色突出显示表示当前线程是该框的一部分。 当前线程还由工具提示中的粗体堆栈帧指示。 如果在“线程”窗口中双击“主线程”,则可以观察到 并行堆栈 窗口中的突出显示箭头会相应地移动。
继续执行,直到第二个断点
若要在达到第二个断点之前继续执行,请在 “调试” 菜单上选择“ 继续”。 下图显示了第二个断点处的线程树。
在第一个断点时,四个线程全部经过从 S.A 到 S.B 再到 S.C 的方法。 该信息仍显示在 “并行堆栈” 窗口中,但四个线程已进一步推进。 其中一个继续到 S.D,然后到 S.E。另一个继续到 S.F、S.G 和 S.H。另外两人继续到 S.I 和 S.J,其中一人前往 S.K,另一个继续到非用户的外部代码。
可以将鼠标悬停在堆栈帧上以查看线程 ID 和其他帧详细信息。 蓝色突出显示表示当前线程,黄色箭头指示当前线程的活动堆栈帧。
可以将鼠标悬停在框标题上,例如 1 个线程 或 2 个线程,以查看线程的线程 ID。 可以将鼠标悬停在堆栈帧上以查看线程 ID 和其他帧详细信息。 蓝色突出显示表示当前线程,黄色箭头指示当前线程的活动堆栈帧。
线缆图标(交织线)表示非当前线程的活动栈帧。 在 “调用堆栈 ”窗口中,双击 S.B 切换帧。 “并行堆栈”窗口使用曲线箭头图标指示当前线程的当前堆栈帧。
注释
有关“并行堆栈”窗口中所有图标的说明,请参阅 “使用并行堆栈”窗口。
在“ 线程 ”窗口中,在线程之间切换并观察 “并行堆栈” 窗口中的视图已更新。
可以使用 “并行堆栈” 窗口中的快捷菜单切换到另一个线程或另一个线程的另一帧。 例如,右键单击 S.J,指向“ 切换到帧”,然后选择命令。
右键单击 S.C 并指向“ 切换到帧”。 其中一个命令具有一个复选标记,指示当前线程的堆栈帧。 可以切换到同一线程的帧(只有曲线箭头移动),也可以切换到另一个线程(蓝色突出显示也会移动)。 下图显示了子菜单。
当方法上下文仅与一个堆栈帧相关联时,框标头显示 1 个 Thread ,可以通过双击切换到它。 如果双击一个与方法相关联且有超过1个帧的框架的上下文,菜单会自动弹出。 将鼠标悬停在方法上下文上时,请注意右侧的黑色三角形。 单击该三角形还会显示快捷菜单。
对于具有多个线程的大型应用程序,你可能只想关注一部分线程。 “并行堆栈”窗口只能显示已标记线程的调用堆栈。 若要标记线程,请使用快捷菜单或线程的第一个单元格。
在工具栏上,选择列表框旁边的 “仅显示标记” 按钮。
现在,只会在 “并行堆栈” 窗口中显示已标记的线程。
继续执行,直到第三个断点
若要在命中第三个断点之前继续执行,请在 “调试” 菜单上选择“ 继续”。
当多个线程位于同一方法中,但该方法不在调用堆栈的开头时,该方法将显示在不同的框中。 当前断点的一个示例是 S.L,它包含三个线程,并显示在三个框中。 双击 S.L。
请注意,S.L 在其他两个框中为粗体,便于你查看其出现的其他位置。 如果想查看哪些帧调用 S.L,以及 S.L 调用了哪些帧,请选择工具栏上的“切换方法视图”按钮。 下图显示了 “并行堆栈” 窗口的方法视图。
请注意图表如何围绕所选方法旋转,并将其放置在视图的中央自己的框中。 呼叫者和被呼叫者分别显示在顶部和底部。 再次选择“切换方法视图”按钮以退出此模式。
“并行堆栈”窗口的快捷菜单还具有以下其他项。
十六进制显示 在工具提示中的数字在十进制和十六进制之间切换。
符号设置 打开相应的对话框。
在源中显示线程 切换源代码中线程标记的显示,其中显示了源代码中线程的位置。
显示外部代码 显示所有帧,即使它们不在用户代码中也是如此。 试一下查看图形是否展开以容纳其他框架(可能为灰色,因为你没有相应的符号)。
在 “并行堆栈 ”窗口中,确保工具栏上的 “自动滚动到当前堆栈帧 ”按钮处于打开状态。
当你有大型图表并进入下一个断点时,你可能希望视图自动滚动到当前线程的活动堆栈帧;即,首先命中断点的线程。
在继续之前,请在 “并行堆栈” 窗口中,向左滚动到底,然后向下滚动到底。
继续执行,直到第四个断点
若要继续执行,直到命中第四个断点,请在 “调试” 菜单上选择“ 继续”。
请注意视图是如何自动滚动就位的。 在 “线程” 窗口中切换线程或切换 “调用堆栈 ”窗口中的堆栈帧,并注意视图如何始终自动滚动到正确的帧。 关闭 “自动滚动到当前工具帧 ”选项并查看差异。
鸟瞰图还有助于在“并行堆栈”窗口中使用大型图表。 默认情况下, 鸟瞰图 处于打开状态。 但是,可以通过单击窗口右下角滚动条之间的按钮来切换它,如下图所示。
在鸟瞰图中,可以移动矩形以快速平移关系图。
在任意方向移动图表的另一种方法是选择图表的空白区域并将其拖动到所需位置。
若要放大和缩小图表,请在移动鼠标滚轮时按住 Ctrl。 或者,选择工具栏上的“缩放”按钮,然后使用“缩放”工具。
还可以通过单击 “工具” 菜单、单击“ 选项”,然后选择或清除 调试 节点下的选项,而不是从上到下查看堆栈。
在继续之前,在 “调试 ”菜单上,选择“ 停止调试 ”以结束执行。
使用“并行任务”窗口和“并行堆栈窗口的任务视图”
我们建议您在继续之前完成前面的步骤。
在达到第一个断点之前重启应用程序:
在 “调试 ”菜单上,选择“ 开始调试 ”并等待第一个断点命中。
在 “调试 ”菜单上,指向 Windows ,然后选择“ 线程”。 将线程窗口停靠在 Visual Studio 底部。
在 “调试” 菜单上,指向 Windows 并选择 “调用堆栈”。 在 Visual Studio 底部停靠 “调用堆栈” 窗口。
双击“ 线程 ”窗口中的线程使其保持最新状态。 当前的线程标记为黄色箭头。 更改当前线程时,会更新其他窗口。 接下来,我们将分析任务。
在 “调试 ”菜单上,指向 Windows,然后选择“ 任务”。 下图显示了 “任务” 窗口。
对于每个正在运行的任务,可以读取其 ID,该 ID 通过同名属性返回,还可以读取运行该任务的线程的 ID 和名称,其位置悬停时会显示拥有完整调用堆栈的工具提示。 此外,在 “任务” 列下,可以看到传递到任务的方法;换句话说,起点。
可以对任何列进行排序。 请注意指示排序列和方向的排序图标。 还可以通过向左或向右拖动来重新排列列的顺序。
黄色箭头指示当前任务。 可以通过双击任务或使用快捷菜单来切换任务。 切换任务时,基础线程将变为当前状态,并更新其他窗口。
手动从一个任务切换到另一个任务时,箭头大纲指示非当前任务的当前调试器上下文。
手动从一个任务切换到另一个任务时,黄色箭头将移动,但白色箭头仍显示导致调试器中断的任务。
继续执行,直到第二个断点
若要在达到第二个断点之前继续执行,请在 “调试” 菜单上选择“ 继续”。
以前, “状态 ”列将所有任务显示为“活动”,但现在其中两个任务被阻止。 由于 多种不同原因,可以阻止任务。 在 “状态 ”列中,将鼠标悬停在等待任务上,了解它被阻止的原因。 例如,在下图中,任务 11 正在等待任务 12。
以前, “状态 ”列将所有任务显示为“活动”,但现在其中两个任务被阻止。 由于 多种不同原因,可以阻止任务。 在 “状态 ”列中,将鼠标悬停在等待任务上,了解它被阻止的原因。 例如,在下图中,任务 4 正在等待任务 5。
反过来,任务 4 正在等待分配给任务 2 的线程拥有的监视器。 (右键单击标题行,然后选择 “列”,在“线程分配”中查看任务 2 的线程分配值)。
可以通过单击 “任务” 窗口的第一列中的标志来标记任务。
可以使用标记来跟踪同一调试会话中不同断点之间的任务,或筛选调用堆栈显示在 并行堆栈 窗口中的任务。
之前使用 “并行堆栈” 窗口时,你查看了应用程序线程。 再次查看 “并行堆栈” 窗口,但这次查看应用程序任务。 通过在左上角的框中选择 任务 来进行操作。 下图显示了“任务视图”。
当前未执行任务的线程不会显示在 “并行堆栈” 窗口的任务视图中。 此外,对于执行任务的线程,某些与任务无关的堆栈帧将从堆栈的顶部和底部进行筛选。
再次查看 “任务” 窗口。 右键单击任何列标题以查看该列的快捷菜单。
可以使用快捷菜单添加或删除列。 例如,未选择 AppDomain 列;因此,它不会显示在列表中。 选择 父。 父列显示在四个任务中的任何一个没有值的情况下。
继续执行直到第三个断点
若要在命中第三个断点之前继续执行,请在 “调试” 菜单上选择“ 继续”。
在此示例中,请注意任务 11 和任务 12 在同一线程上运行(如果隐藏,则显示 “线程分配 ”列)。 此信息不会显示在 “线程” 窗口中;在此处看到这是 “任务” 窗口的另一个好处。 若要确认这一点,请查看 “并行堆栈” 窗口。 请确保正在查看 任务。 可以通过扫描 并行堆栈 窗口上的工具提示来查找任务 11 和 12。
新的任务 5 现在正在运行,任务 4 现在正在等待。 可以通过将鼠标悬停在 “状态” 窗口中的等待任务上来了解原因。 在“父任务”列中,请注意任务 4 是任务 5 的父任务。
若要更好地可视化父子关系,请右键单击列标题行,然后选择 “父子视图”。 应会看到下图。
请注意,任务 4 和任务 5 正在同一线程上运行(如果 线程分配 列被隐藏,请将其显示出来)。 此信息不会显示在 “线程” 窗口中;在此处看到这是 “任务” 窗口的另一个好处。 若要确认这一点,请查看 “并行堆栈” 窗口。 请确保正在查看 任务。 通过在 “任务” 窗口中双击任务 4 和 5 来查找任务 4 和 5。 执行此操作时,并行堆栈窗口中的蓝色突出显示将更新。 还可以通过在 “并行堆栈 ”窗口中扫描工具提示来查找任务 4 和 5。
在 “并行堆栈” 窗口中,右键单击 S.P,然后选择“ 转到线程”。 窗口切换到“线程视图”,相应的帧处于视图中。 可以在同一讨论线程上看到这两个任务。
与“线程”窗口相比,这是“并行堆栈”窗口中“任务视图”的另一个好处。
继续执行,直到第四个断点
若要在命中第三个断点之前继续执行,请在 “调试” 菜单上选择“ 继续”。 选择 ID 列标题以按 ID 排序。 应会看到下图。
任务 10 和任务 11 现在正在等待对方,并被阻止。 还有一些新任务现在已计划。 计划任务是已在代码中启动但尚未运行的任务。 因此,它们的 “位置” 和 “线程分配 ”列显示默认消息或为空。
由于任务 5 已完成,因此不再显示它。 如果计算机上不存在这种情况,并且未显示死锁,请按 F11 一步。
任务 3 和任务 4 现在正在等待彼此,已被阻塞。 还有 5 个新任务是任务 2 的子任务,现在已安排。 计划任务是已在代码中启动但尚未运行的任务。 因此,它们的位置和线程分配列为空。
再次查看 “并行堆栈” 窗口。 每个框的标头都有一个显示线程 ID 和名称的工具提示。 切换到 “并行堆栈 ”窗口中的任务视图。 将鼠标悬停在标头上以查看任务 ID 和名称以及任务的状态,如下图所示。
可以按列对任务进行分组。 在 “任务” 窗口中,右键单击 “状态 ”列标题,然后选择“ 按状态分组”。 下图显示了按状态分组的 “任务” 窗口。
还可以按任何其他列进行分组。 通过对任务进行分组,可以专注于一部分任务。 每个可折叠组都有组内项的数量。
在 任务窗口 中需要查看的最后一项功能是你右键单击任务时显示的快捷菜单。
快捷菜单显示不同的命令,具体取决于任务的状态。 这些命令可能包括复制、全选、十六进制显示、切换到任务、冻结分配的线程、冻结除当前线程之外的所有线程、解冻分配的线程和标记。
可以冻结任务或任务的基础线程,也可以冻结除分配的线程以外的所有线程。 在 “任务” 窗口中,冻结的线程与在 “线程” 窗口中一样,用蓝色 暂停 图标来表示。
概要
本演练演示了 并行任务 和 并行堆栈 调试器窗口。 在使用多线程代码的实际项目中使用这些窗口。 可以检查用 C++、C# 或 Visual Basic 编写的并行代码。