分类 调试 下的文章

使用“并行堆栈”窗口:“线程”视图

在“调试”菜单上,单击“启动调试”。 等待命中第一个断点。

查看一个线程的调用堆栈

  1. 在“调试”菜单上指向“窗口”,然后单击“线程”。 将“线程”窗口停靠在 Visual Studio 底部。

  2. 在“调试”菜单上指向“窗口”,然后单击“调用堆栈”。 将“调用堆栈”窗口停靠在 Visual Studio 底部。

  3. 在“线程”窗口中双击线程,使其作为当前线程。 当前线程具有一个黄色箭头。 更改当前线程时,其调用堆栈显示在“调用堆栈”窗口中。

查看“并行堆栈”窗口

  1. 在“调试”菜单上指向“窗口”,然后单击“并行堆栈”。 确保在左上角的框中选择了“线程”。

    通过使用 "并行堆栈" 窗口,您可以在一个视图中同时查看多个调用堆栈。 下图显示了 "调用堆栈" 窗口上方的 "并行堆栈" 窗口。

    "并行堆栈" 窗口中的线程视图

    主线程的调用堆栈显示在一个框中,其他四个线程的调用堆栈则划分到另一个框中。 将这四个线程划分在一起是因为其堆栈帧共享相同的方法上下文;也就是说,它们处于相同的方法中:ABC若要查看共享同一框的线程的 Id 和名称,请将鼠标悬停在带有标题的框上(4 个线程)。 当前线程以粗体显示。

    显示线程 Id 和名称的工具提示

    黄色箭头指示当前线程的活动堆栈帧。

    通过右键单击“调用堆栈”窗口,可以设置要显示堆栈帧的多少详细信息(“模块名称”、“参数类型”、“参数名称”、“参数值”、“行号”和“字节偏移量”)。

    方框周围的蓝色突出显示指示当前线程是该框的一部分。 工具提示中的粗体堆栈帧也可指示当前线程。 如果双击“线程”窗口中的主线程,可以看到“并行堆栈”窗口中的蓝色突出显示内容相应移动。

    并行堆栈窗口中突出显示的主线程

继续执行到第二个断点

  1. 若要继续执行到命中第二个断点,请在“调试”菜单上单击“继续”。 下图所示为第二个断点处的线程树。

    显示多个分支的并行堆栈窗口

    在第一个断点处,所有四个线程均从 S.A 执行到 S.B 再到 S.C 方法。 该信息仍会显示在“并行堆栈”窗口中,但是这四个线程已进一步执行。 其中一个线程继续执行到 S.D 再到 S.E。 另一个线程继续执行到 S.F、S.G 和 S.H。 其余两个线程继续执行到 S.I 和 S.J,其中一个线程从此方法执行到 S.K,而另一个线程继续执行到非用户外部代码。

    可以将鼠标指针悬停在框标题(例如,“1 个线程”或“2 个线程”)上,以查看线程的线程 ID。 将鼠标指针悬停在堆栈帧上可以查看线程 ID 和其他帧详细信息。 蓝色突出显示指示当前线程,黄色箭头指示当前线程的活动堆栈帧。

    软线程图标(interweaved lines)指示非当前线程的活动堆栈帧。 在“调用堆栈”窗口中,双击 S.B 可以切换帧。 “并行堆栈”窗口通过使用绿色的曲线箭头图标指示当前线程的当前堆栈帧。

    在“线程”窗口中进行线程切换时,“并行堆栈”窗口中的视图相应更新。

    可通过在“并行堆栈”窗口中使用快捷菜单来切换到其他线程或其他线程的其他帧。 例如,右键单击 S.J,指向“切换到帧”,然后单击某一命令。

    并行堆栈执行路径

    右键单击 S.C,指向“切换到帧”。 其中一个带有选中标记的命令指示当前线程的堆栈帧。 您可以切换到相同线程的上述帧(将仅移动绿色箭头),也可以切换到其他线程(将同时移动蓝色突出显示)。 下图所示为子菜单。

    在 C 上具有2个选项的堆栈菜单,而 J 是最新的

    当方法上下文只与一个堆栈帧关联时,框标题显示“1 个线程”,双击它即可切换到该线程。 如果双击关联有一个以上的帧的方法上下文,则会自动弹出该菜单。 将鼠标指针悬停在方法上下文上时,请注意右侧的黑色三角形。 单击该三角形也可以显示该快捷菜单。

    对于具有多个线程的大型应用程序,您可能只希望关注某个线程子集。 “并行堆栈”窗口可仅显示已标记线程的调用堆栈。 若要标记线程,请使用快捷菜单或线程的第一个单元格。

    在工具栏上,单击列表框旁边的“仅显示已标记项”按钮。

    并行堆栈窗口和工具提示

    现在,只有已标记的线程才会显示在 "并行堆栈" 窗口中。

继续执行到第三个断点

  1. 若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。

    如果有多个线程位于同一方法中,但该方法不在调用堆栈开头,则会在不同框中显示该方法。 当前断点处的一个示例是 S.L,它包含三个线程并在三个框中显示这三个线程。 双击 S.L。

    "并行堆栈" 窗口中的执行路径

    请注意,S.L 在其他两个框中为粗体,这样您可以看到 S.L 的其他显示位置。 如果希望查看对 S.L 进行调用的帧和 S.L 所调用的帧,请单击工具栏上的“切换方法视图”按钮。 下图显示了 "并行堆栈" 窗口的 "方法" 视图。

    "并行堆栈" 窗口中的方法视图

    请注意以上关系图以所选方法为中心并将其单独放在视图中间的方框中。 被调用方和调用方分别显示在顶部和底部。 再次单击“切换方法视图”按钮可以退出该模式。

    “并行堆栈”窗口的快捷菜单还包括以下其他项。


    • “十六进制显示”,用于在十进制和十六进制之间切换工具提示中的数字。

    • 符号设置打开各自的对话框。

    • 在源中显示线程在源代码中切换线程标记的显示,这些线程标记显示源代码中的线程位置。

    • “显示外部代码”,用于显示所有帧(即使这些帧未处于用户代码中)。 使用此项可查看展开的关系图,其中包含其他帧(这些帧可能因没有相应符号而灰显)。

  2. 在“并行堆栈”窗口中,确保已启用工具栏上的“自动滚动到当前堆栈帧”按钮。

    如果关系图较大,当单步执行到下一断点时,您可能希望视图自动滚动到当前线程的活动堆栈帧;即第一个命中该断点的线程。

  3. 继续之前,请在“并行堆栈”窗口中一直滚动到左侧和底部。

继续执行到第四个断点

  1. 若要继续执行到命中第四个断点,请在“调试”菜单上单击“继续”。

    请注意视图如何自动滚动到位。 在“线程”窗口中切换线程或在“调用堆栈”窗口中切换堆栈帧,并注意视图如何一直自动滚动到正确的帧。 禁用“自动滚动到当前工具帧”选项并查看不同之处。

    “鸟瞰图”还有助于在“并行堆栈”窗口中处理大型关系图。 默认情况下,鸟瞰图为 on。 但是,您可以通过单击窗口右下角滚动条之间的按钮来对其进行切换,如下图所示。

    "并行-堆栈" 窗口中的鸟瞰视图

    在鸟瞰视图中,你可以移动矩形以便在关系图中快速平移。

    朝任意方向移动此关系图的另一种方法是:单击此关系图的空白区域并将其拖动到所需位置。

    若要放大和缩小此关系图,请按住 Ctrl 并移动鼠标滚轮。 或者,也可以单击工具栏上的“缩放”按钮并使用缩放工具。

    通过单击“工具”菜单和“选项”,并选中或清除“调试”节点下的相应选项,还可以按从上到下(而不是从下到上)的方向查看堆栈。

  2. 继续之前,请在“调试”菜单上单击“停止调试”以结束执行。

使用“并行任务”窗口和“并行堆栈”窗口的任务视图

继续之前,建议您先完成前面的过程。

重新启动应用程序,直到命中第一个断点

  1. 在“调试”菜单上,单击“启动调试”并等待命中第一个断点。

  2. 在“调试”菜单上指向“窗口”,然后单击“线程”。 将“线程”窗口停靠在 Visual Studio 底部。

  3. 在“调试”菜单上指向“窗口”,然后单击“调用堆栈”。 将“调用堆栈”窗口停靠在 Visual Studio 底部。

  4. 在“线程”窗口中双击线程,以将其作为当前线程。 当前线程具有一个黄色箭头。 更改当前线程时,将更新其他窗口。 接着,我们将查看任务。

  5. 在 "调试" 菜单上,指向 "窗口",再单击 "任务"。 下图显示了 "任务" 窗口。

    "任务" 窗口中的四个正在运行的任务

    对于运行的每一项任务,您可以读取其 ID(由名称相同的属性返回)、运行该任务的线程的 ID 和名称以及任务位置(悬停以显示具有整个调用堆栈的工具提示)。 此外,“任务”列下还会显示传递到任务中的方法,即起始点。

    可以对任何列进行排序。 请注意指示排序列和方向的排序标志符号。 您还可以通过向左或向右拖动列来对列重新排序。

    黄色箭头指示当前任务。 通过双击某一任务或使用快捷菜单可以切换任务。 切换任务时,基础线程即成为当前线程并更新其他窗口。

    在不同任务之间进行手动切换时,黄色箭头将相应移动,而白色箭头仍显示导致调试器中断的任务。

继续执行到第二个断点

  1. 若要继续执行到命中第二个断点,请在“调试”菜单上单击“继续”。

    之前,"状态" 列将所有任务显示为 "活动",但现在两个任务被阻止。 任务可能因多种不同原因而被阻止。 在“状态”列中,将鼠标指针悬停在正在等待的任务上可以查看其阻止原因。 例如,在下图中,任务 3 正在等待任务 4。

    "任务" 窗口中的两个等待任务

    任务 4 又在等待分配给任务 2 的线程所拥有的监视器。 (右键单击标题行,然后选择 "" > 线程分配查看任务2的线程分配值 ")。

    "任务" 窗口中的等待任务和工具提示

    可以通过单击 "任务" 窗口第一列中的标志来标记任务。

    使用标记功能可以跟踪同一调试会话中的不同断点之间的任务或筛选“并行堆栈”窗口中显示的调用堆栈所对应的任务。

    在先前使用“并行堆栈”窗口时,已查看了应用程序线程。 请再次查看“并行堆栈”窗口,但这次查看应用程序任务。 请在左上方的框中选择“任务”来执行此操作。 下图所示为任务视图。

    "并行堆栈" 窗口中的 "任务" 视图

    当前未在执行任务的线程不会显示在“并行堆栈”窗口的任务视图中。 此外,对于执行任务的线程,某些与任务无关的堆栈帧将从堆栈的顶部和底部筛选掉。

    再次查看 "任务" 窗口。 右击任何列标题可以查看该列的快捷菜单。

    您可以使用此快捷菜单添加或移除列。 例如,AppDomain 列未处于选中状态;因此,不会在列表中显示它。 单击“父级”。 此时将显示“父级”列,但不显示四个任务中任何任务的值。

继续执行到第三个断点

  1. 若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。

    此时,新任务(即任务 5)正在运行,而任务 4 则处于等待状态。 通过将鼠标指针悬停在“状态”窗口中正在等待的任务上,可以查看其原因。 请注意,列中的任务4是任务5的父项。

    若要更好地直观显示父子关系,请右键单击列标题行,然后单击 "父子视图"。 您应看到以下图示。

    "-任务" 窗口中的父子视图

    请注意,任务4和任务5正在同一线程上运行(如果它是隐藏的,则显示 "线程分配" 列)。 此信息不会显示在 "线程" 窗口中;这里看到的是 "任务" 窗口的另一个优点。 为确认这一点,请查看“并行堆栈”窗口。 确保你正在查看“任务”。 任务窗口中双击任务4和5。 执行此操作时,“并行堆栈”窗口中的蓝色突出显示内容随之更新。 此外,还可以通过扫描“并行堆栈”窗口上的工具提示来查找任务 4 和任务 5。

    "并行堆栈" 窗口中的任务视图

    在“并行堆栈”窗口中右键单击 S.P,然后单击“转到线程”。 此时,窗口将切换到线程视图,并显示相应帧。 你可以在同一线程上查看两个任务。

    线程视图中突出显示的线程

    这是“并行堆栈”窗口中的任务视图相较于“线程”窗口的另一优势。

继续执行到第四个断点

若要继续执行到命中第三个断点,请在“调试”菜单上单击“继续”。 单击要按 ID 排序的“ID”列标题。 您应看到以下图示。

"并行堆栈" 窗口中的四个任务状态

由于任务 5 已完成,因此不再显示该任务。 如果计算机上并非如此并且未显示死锁,请通过按“F11”单步执行一次。

任务3和任务4现在正在等待,并被阻止。 此外,还存在 5 个作为任务 2 的子级的新任务,目前已计划这些任务。 已计划任务是已在代码中启动但尚未运行的任务。 因此,其“位置”和“线程分配”列为空。

再次查看“并行堆栈”窗口。 每个框的标题都具有一个显示线程 ID 和名称的工具提示。 在“并行堆栈”窗口中切换到任务视图。 将鼠标指针悬停在标题上可以查看任务 ID 和名称以及任务状态,如下图所示。

"并行堆栈" 窗口中的标题工具提示

可以按列对任务进行分组。 在 "任务" 窗口中,右键单击 "状态" 列标题,然后单击 "按状态分组"。 下图显示按状态分组的 "任务" 窗口。

"任务" 窗口中的分组任务

此外,还可以按其他列进行分组。 通过对任务进行分组,你可以关注某个任务子集。 每个可折叠的组都包含一个分组在一起的项的计数。

要检查的任务窗口的最后一项功能是右键单击任务时显示的快捷菜单。

快捷菜单根据任务状态显示不同的命令。 这些命令可能包括“复制”、“全选”、“十六进制显示”、“切换到任务”、“冻结指定的线程”、“冻结此线程之外的全部线程”、“解冻指定的线程”和“标志”。

你可以冻结一个或多个任务的基础线程,也可以冻结除指定线程外的所有线程。 冻结的线程在 "任务" 窗口中以 "线程" 窗口中的蓝色暂停图标表示。

当在C++语言中引入异常时,引入了相应的throw(…)动态异常说明符,注释了哪些异常可以由函数抛出。比如:

// this function might throw an integer or a pointer to char,
// but nothing else.
void foo() throw(int, char*);

线程是操作系统向其授予处理器时间的指令序列。 在操作系统中运行的每个进程都包含至少一个线程。 包含多个线程的进程称为多线程。有多个处理器、多核处理器或超线程进程的计算机可以同时运行多个线程。 使用多个线程的并行处理可以极大地提高程序性能,但也可能导致调试变得更加困难,因为正在跟踪多个线程。

多线程处理可能会引入新类型的潜在 bug。 例如,两个或多个线程可能需要访问同一资源,但是一次只能有一个线程可以安全地访问该资源。 需要某种形式的互斥才能确保每次只有一个线程访问该资源。 如果未正确实现互斥,则可能会创建不会执行任何线程的死锁情况。 死锁通常是一个难以调试的问题。

用于调试多线程应用的工具

Visual Studio 提供不同的工具用于调试多线程应用程序。

  • 对于线程,调试线程的主要工具有 "线程" 窗口、源窗口中的线程标记、"并行堆栈" 窗口、"并行监视" 窗口和 "调试位置" 工具栏。

  • 对于使用任务并行库(TPL)或并发运行时的代码,用于调试的主要工具是 "并行堆栈" 窗口、"并行监视" 窗口和 "任务" 窗口,该窗口还支持JavaScript. 

  • 对于调试 GPU 上的线程,主要工具是“GPU 线程”窗口。

  • 对于进程,主要工具是“附加到进程”对话框、“进程”窗口和“调试位置”工具栏。

Visual Studio 还提供功能强大的断点和跟踪点,在调试多线程应用程序时,这会很有用。 使用断点条件和筛选器将断点置于单个线程上。 使用跟踪点可以在不中断的情况下跟踪程序的执行,从而研究死锁之类的问题。

调试具有用户界面的多线程应用程序可能会特别困难。 可以考虑在另一台计算机上运行应用程序并使用远程调试。 

使用 "线程" 窗口调试多线程应用

多个 Visual Studio 用户界面元素可帮助调试多线程应用。 下面介绍 "代码编辑器" 窗口、"调试位置" 工具栏和 "线程" 窗口中的多线程调试功能。 

“启动调试”

  1. 代码行上设置断点,方法是单击左侧的滚动条线,或选择线条并按F9。断点在代码行旁边的左侧滚动条中显示为红色圆圈。

  2. 选择 "调试" > "开始调试",或按F5。

    应用程序在调试模式下启动,并在断点处暂停。

  3. 在中断模式下,通过选择 "调试" > Windows > 线程打开 "线程" 窗口。 你必须在调试会话中才能打开或查看线程和其他调试窗口。

检查线程标记

  1. "线程" 窗口中右键单击,然后选择菜单中的 "在源中显示线程"

    源代码行旁边的装订线现在显示一个线程标记图标线程标记线程标记指示线程在此位置停止。 如果该位置有多个已停止的线程,则会显示多个线程图标。

  2. 将指针悬停在线程标记上。 显示数据提示,并显示已停止的线程或线程的名称和线程 ID 号。 线程名称可能 <No Name>

    为了帮助识别不需要的线程,您可以在 "线程" 窗口中重命名它们。 右键单击该线程,然后选择 "重命名"。

STATUS_STACK_BUFFER_OVERRUN异常一般是指栈缓冲区溢出的溢出,代码为0xC0000409,消息提示一般为“Security check failure or stack buffer overrun”。微软给出的解释如下:系统在应用程序中检测到基于堆栈的缓冲区溢出。此溢出可能允许恶意用户获得此应用程序的控制权。/GS编译器开关允许创建堆栈“cookie”值,该值用作检测堆栈缓冲区溢出的金丝雀。如果cookie已损坏,则进程会立即终止自身,状态代码为STATUS_STACK_BUFFER_OVERRUN。

在做如上描述的时后,STATUS_STACK_BUFFER_OVERRUN是一个好名字。但是,随着时间的推移,随着我们遇到和处理问题越来越多,我们会发现其他立即终止进程的原因。其中一些与安全有关其他的没有。比如在遇到更糟糕的情况下希望在进程终止前立即停止,用fail fast这个词是用来形容这种情况的就非常好。甚至还有一个编译器内部函数来终止使用这个特殊状态代码的当前进程

似乎人们无法就是fail fast还是fast fail达成一致,所以在实践中,你可以看到这两种变化。这意味着现在当你得到一个STATUS_STACK_BUFFER_OVERRUN时,实际上并不意味着存在堆栈缓冲区溢出。这只意味着应用程序决定以极快的速度自行终止。

如果你查看winnt.h文件,您将看到各种快速失败代码的列表。这些代码指示发生了哪种类型的快速故障。

#define FAST_FAIL_LEGACY_GS_VIOLATION         0
#define FAST_FAIL_VTGUARD_CHECK_FAILURE       1
#define FAST_FAIL_STACK_COOKIE_CHECK_FAILURE  2
#define FAST_FAIL_CORRUPT_LIST_ENTRY          3
#define FAST_FAIL_INCORRECT_STACK             4
#define FAST_FAIL_INVALID_ARG                 5
#define FAST_FAIL_GS_COOKIE_INIT              6
#define FAST_FAIL_FATAL_APP_EXIT              7