线程性能:Visual Studio 2010 中的资源争用并发分析


随着多核处理器变得越来越常见,软件开发人员就需要构建多线程应用程序,从而利用更多的处理能力来实现更高的性能。借助并行线程的强大功能,您可以将整个工作划分为多项单独的任务,并以并行模式执行这些任务。

  但是,线程经常需要相互通信才能完成任务;而且根据算法或数据访问的要求,有时还需要同步线程的行为。例如,应该以互斥的方式授予线程对同一数据的同时写访问权限,以避免数据损坏。

  同步操作通常是通过使用共享的同步对象来完成的。对于获得该对象的线程,将授予其对敏感代码或数据的共享或独占访问权限。当不再需要访问权限时,该线程将放弃所有权,而其他线程就可以尝试获取访问权限。根据所使用的同步类型,同时请求所有权可能会使多个线程能够同时访问共享资源,也可能会阻止某些线程,直到共享对象从上一次获取中释放为止。具体的示例包括:C/C++ 中使用 EnterCriticalSection 和 LeaveCriticalSection 访问例程的关键部分,C/C++ 中的 WaitForSingleObject 函数,以及 C# 中的锁定语句和 Monitor 类。

  选择同步机制时必须谨慎,因为不恰当的线程同步不但不能提高性能,反而会降低性能,这与多线程的目标背道而驰。因此,能否检测由于锁争用没有进展而导致线程被阻止的情况,就显得愈加重要。

  Visual Studio 2010 中的性能工具包含一种新的分析方法“资源争用分析”,此方法有助于检测线程间的并发争用。在 John Robbins 的 Wintellect 博客文章中,您可以看到对此功能的精彩简介,其网址为:wintellect.com/CS/blogs/jrobbins/archive/2009/10/19/vs-2010-beta-2-concurrency-resource-profiling-in-depth-first-look.aspx。

  在本文中,我将演练一个争用分析调查,并讲解可以使用 Visual Studio 2010 IDE 和命令行工具收集的数据。此外,还将向您展示如何在 Visual Studio 2010 中分析数据,您会看到,在执行争用调查时,如何从一个分析视图切换到另一个分析视图。然后,我会修改代码,并将修改过的应用程序的分析结果与原来的分析结果进行比较,验证所做的修改确实减少了争用的数量。

  从问题开始

  作为示例,我将使用 Hazim Shafi 在其博客文章“性能模式 1:识别锁争用”(blogs.msdn.com/hshafi/archive/2009/06/19/performance-pattern-1-identifying-lock-contention.aspx) 中使用的同一个矩阵乘法应用程序。尽管代码示例是用 C++ 编写的,但是我讨论的概念同样适用于托管代码。

  示例矩阵乘法应用程序使用多个线程对两个矩阵执行乘法操作。每个线程都将承担一部分工作,并运行以下代码段:

for (i = myid*PerProcessorChunk; 
   i < (myid+1)*PerProcessorChunk; 
   i++) { 
 EnterCriticalSection(&mmlock); 
 for (j=0; j<SIZE; j++) { 
  for (k=0; k<SIZE; k++) { 
   C[i][j] += A[i][k]*B[k][j]; 
  } 
 } 
 LeaveCriticalSection(&mmlock); 
}

  每个线程都有自己的 ID (myid),并且负责使用矩阵 A 和 B 作为输入,来计算结果矩阵 C 中的行数(一行或多行)。深入的代码检测表明,没有发生真正引起歧义的写共享,每个线程都写入 C 的不同行。然而,开发人员还是决定使用关键部分来保证对矩阵的赋值。我要感谢开发人员的这个决定,因为这给了我一个好机会,来展示新的 Visual Studio 2010 性能工具能够轻松发现冗余的同步。

  分析数据集合

  假设您有一个 Visual Studio 项目,其中的代码如上文所示(但这不是必需的,因为您可以将分析器连接到任何一个处在运行中的应用程序),您单击“分析”菜单上的“启动分析向导”开始进行争用分析。

  在该向导的第一个页面上(如图 1 所示),选择“并发”,并确保选中“收集资源争用数据”选项。请注意,资源争用并发分析适用于任何版本的 Windows 操作系统。但“显示多线程应用程序的行为”选项仅适用于 Windows Vista 或 Windows 7。


图 1 启用并发资源分析

  在该向导的第二个页面上,确保目标是当前项目。在该向导的最后一个页面上,确保选中“在向导完成后启动分析”选项,然后单击“完成”。应用程序就会开始运行并由分析器进行分析。当应用程序退出后,“性能资源管理器”窗口中将显示分析数据文件(请参见图 2)。


图 2 性能资源管理器中的性能分析结果文件

  分析报告将在 Visual Studio 中自动打开,并在“摘要”视图中显示性能调查结果,如图 3 所示。


图 3 分析报告的“摘要”视图

  分析数据分析

  并非所有同步都会导致锁争用。如果有一个锁可用,则尝试获取该锁的所有权不会阻止线程执行,也不会发生争用。在“资源争用分析”模式下,分析器只会收集导致争用的同步事件数据,而不会报告成功的(未被阻止的)资源获取操作。如果您的应用程序没有导致任何争用,也就不会收集到任何数据。如果您得到了数据,则意味着您的应用程序存在锁争用现象。

  对于每次争用,分析器会报告被阻止的线程、发生争用的位置(资源和调用堆)、发生争用的时间(时间戳),以及被阻止的线程尝试执行某些操作的时间量(时间长度),这些操作包括获取锁、进入关键部分、等待单个对象等。

  当您打开该文件时,首先会看到“摘要”视图(图 3),其中包含三个主要区域,可供您进行简单的诊断:

  争用图显示了每秒钟发生的争用次数,将在应用程序的生命周期内绘制。您可以直观地检查争用峰值,也可以选择一个时间间隔,以便详细观察争用或筛选结果。筛选操作将再次对数据进行分析,并删除所选时间间隔以外的数据。

  “争用最多的资源”表列出了导致所检测到的大部分争用的资源。

  “争用最多的线程”表列出了发生争用次数最多的线程。此表使用争用次数而不是争用时间长度作为标准。因此,可能会有一个线程长时间被单个争用所阻止,但该线程不会显示在“摘要”视图中。另一方面,如果一个线程经历了大量非常短的争用,每次争用都只将该线程阻止很短时间,则该线程会出现在“摘要”视图中。

  如果您看到某个资源导致了大部分争用,则应该更详细地检查该资源。如果您观察到某个线程经历了大量您没有预期到的争用,也应该检查该线程的争用情况。

  例如,在图 3 中,您可以看到,关键部分 1 导致了该应用程序中的几乎所有 (99.90%) 的争用。让我们对该资源进行深入调查。

  “摘要”视图上的资源名称和线程 ID 是超链接。单击“关键部分 1”会转到“资源详细信息”视图(请参见图 4),并将上下文设置为特定的资源:关键部分 1。


图 4 “资源详细信息”视图

  资源详细信息

  “资源详细信息”视图的上半部分显示了一个基于时间的图表,其中每条水平线都属于一个线程。除非您在代码中对托管线程进行命名(例如通过使用 C# System.Threading.Thread.Name 属性),否则这些线条将由线程的根函数来定义标签。此类线条上的方块代表相应线程发生的资源争用。方块长度表示争用时间长度。不同线条上的方块在时间上可能会重叠,这意味着几个线程同时被该资源阻止。

  “总计”线条比较特殊。它不属于任何特定的线程,但包含所有线程对该资源的所有争用(它实际上是争用方块在该线条上的投影)。正如您所看到的,关键部分 1 相当繁忙,它在“总计”线条上似乎已经没有任何空位。

  您可以使用鼠标左键来选择时间范围(在图表中左键单击所需的开始点,然后向右拖动指针),从而放大到特定的图表区域。图表的右上部有两个链接:“缩放重置”和“缩小”。“缩放重置”可恢复原始图表视图;“缩小”则会一步步后退,按照您放大的过程逐渐取消缩放。

  总体的争用方块图案可能会帮助您对于应用程序的执行情况得出一些结论。例如,您可以看到,各个线程的争用在时间上大量重叠,表示这不是最优的并行化方案。每个线程被该资源阻止的时间远超过了其运行的时间,而这也是应用程序缺乏效率的另一个标志。

  函数详细信息

  “资源详细信息”视图的底部是一个争用调用堆,只有当您选择了特定的争用时,此处才会显示数据。如果您选择了某个方块,相应的堆就会显示在底部面板中。您也可以悬停在图表上的某个争用方块上,而不单击该方块,这将弹出一个窗口,向您显示堆和争用时间长度信息。

  正如您所看到的,争用调用堆中列出了一个名为 MatMult 的示例应用程序函数,这样您就知道它是导致争用的原因。若要确定是哪一行函数代码导致了争用,请在调用堆面板中双击函数名称。这将显示“函数详细信息”视图,如图 5 所示。


图 5 “函数详细信息”视图

  在此视图中,您会看到名为 MatMult 的函数的图形化表示,以及在此函数内部调用的函数。视图底部清楚地显示出,EnterCriticalSection(&mmlock) 导致了所有的线程被阻止的情况。

  当您知道是哪一行代码导致了争用后,您可能会重新考虑您的决定:是否应该按这种方式来实现同步?这是保护代码的最好方式吗?真的需要保护吗?

  在示例应用程序中,在此代码中使用关键部分是不必要的,因为线程不会共享对同样的结果矩阵行的写权限。利用 Visual Studio 性能工具,您可以将使用 mmlock 的语句变成注释,从而大大提高应用程序的速度。要是始终都这么简单就好了!

  线程详细信息

  如上文所述,“摘要”视图为您的调查提供了一个良好的起点。通过查看“争用最多的资源”和“争用最多的线程”表,您可以决定如何继续调查。如果您发现其中某个线程看起来很可疑,因为您认为它不应该出现在争用最多的线程列表中,您就可能会决定深入检查该线程。

  在“摘要”视图上单击该线程 ID,即可跳转到“线程详细信息”视图(请参见图 6)。尽管看起来与“资源详细信息”视图相似,但是此视图具有不同的含义,因为它会在所选线程的上下文中显示争用情况。每条水平线条代表该线程在其生命周期内争用的一种资源。在此图表上,您不会看到争用方块在时间上重叠,因为这意味着同一个线程在同一时间被多个资源阻止。


图 6 包含所选争用方块的“线程详细信息”视图

  请注意,WaitForMultipleObjects(此处未显示)是单独处理的,对于一组对象,它将用一条线条来表示。这是因为,分析器将 WaitForMultipleObjects 的所有参数对象当作一个实体。

  您能够在“资源详细信息”视图中执行的任何操作(缩放图表、选择特定的争用,以及查看以毫秒为单位的时间长度和调用的堆)也都适用于“线程详细信息”视图。在“争用调用堆”面板中双击函数名称,即可导航到该函数的“函数详细信息”视图。

  在示例中,您可以看到,该线程在执行前期被阻止的时间超过了其运行的时间,然后它会被一组多个句柄长时间阻止。最后一个方块是由于等待其他线程完成而引起的,但是前期的争用表明线程的使用并非最优,从而导致线程处于被阻止状态的时间比处于运行状态的时间还要长。

  找出问题

  您可能已经注意到,图表轴上的标签都是超链接。这样就可以在资源和线程的详细视图之间来回切换,并且每次都能为视图设置所需的上下文。这在交互式查找和解决问题的过程中非常实用。例如,您可以对阻止了许多线程的资源 R1 进行检查。然后从“资源详细信息”视图转到线程 T1 的详细信息视图,并发现它不仅被 R1 阻止,有时还会被资源 R2 阻止。接着,您可以深入了解 R2 的详细信息,并观察被 R2 阻止的所有线程。接下来,您可以单击线程 T2 的标签,集中注意力来检查阻止了 T2 的所有资源;如此这般。

  对于在任意给定时间谁持有锁这个问题,争用分析数据不会给出明确的答案。但是倘若线程之间对同步对象的使用是公平的,并且您对应用程序的行为有所了解,则通过在资源详细信息与线程详细信息之间来回切换,研究其数据,您就可以找出可能的锁持有者(成功获取同步锁的线程)。

  例如,假设在“线程详细信息”视图中,您看到线程 T 在时间 t 处被资源 R 阻止。您可以单击 R 的标签以切换到 R 的“资源详细信息”视图,然后查看在应用程序生命周期内被 R 阻止的所有线程。在时间 t 处,您会看到被 R 阻止的许多线程(包括 T)。而在时间 t 处未被 R 阻止的线程就可能是锁持有者。

  我在上文中说过,图表的“总计”线条是所有争用方块的投影。“总计”标签也是一个超链接,但它会使您从“资源详细信息”视图转到“争用”视图(请参见图 7),该视图包含每种资源对应的争用调用树。并且,适当资源调用树的热路径已激活。该视图在资源调用树中显示了每种资源及每个节点(函数)的争用和阻止时间统计信息。与其他视图不同,该视图将争用堆聚集到资源调用树中(就像在其他分析模式下),并且会提供应用程序总体运行情况的统计信息。


图 7 对关键部分 1 应用了热路径的“争用”视图

  在“争用”视图中,您可以使用上下文菜单返回任何资源的“资源详细信息”视图。指向某个资源,单击鼠标右键,然后选择“显示争用资源详细信息”。使用上下文菜单还可以执行其他有用的操作。一般来说,建议您浏览“分析器”视图中的上下文菜单,因为它们非常有用!

  单击“线程详细信息”视图的“总计”标签可显示“进程”视图,其中选定了相应的线程(请参见图 8)。在该视图中,您可以查看相对于应用程序启动时间的线程启动时间、线程终止时间、线程运行时间长度、线程经历的争用次数,以及线程被所有争用阻止的时间(以毫秒为单位,并且会显示其占线程生命周期的百分比)。


图 8 “进程”视图

  同样,使用上下文菜单可以返回任何线程的“线程详细信息”视图,方法是:选择所需的线程,单击右键,然后选择“显示线程争用详细信息”。

  另一种可行的调查流程是在文件打开时直接显示“进程”视图,然后通过单击某个可用列的标题对线程进行排序(例如,按争用次数对线程进行排序),然后选择某个线程,并使用上下文菜单切换到该线程的争用详细信息图表。

  解决问题并比较结果

  当您找到应用程序中发生锁争用的根本原因后,可以将 mmlock 关键部分变成注释,然后重新运行分析过程:

for (i = myid*PerProcessorChunk; 
   i < (myid+1)*PerProcessorChunk; 
   i++) { 
 // EnterCriticalSection(&mmlock); 
 for (j=0; j<SIZE; j++) { 
  for (k=0; k<SIZE; k++) { 
   C[i][j] += A[i][k]*B[k][j]; 
  } 
 } 
 // LeaveCriticalSection(&mmlock); 
}

  您预期争用次数会减少,而代码修改后的实际分析仅报告了一次锁争用,如图 9 所示。


图 9 代码修改后的分析结果的“摘要”视图

  我们还可以在 Visual Studio 中比较新的性能结果和以前的性能结果。为此,请在性能资源管理器中选择这两个文件(选择一个文件,按住 Shift 或 Ctrl,然后选择另一个文件),然后单击右键,并选择“比较性能报告”。

  此时将显示一个比较报告,如图 10 所示。在示例应用程序中,您可以看到 MatMult 函数包含的争用次数从 1,003 降到了 0。


图 10 比较报告

  其他数据收集方法

  如果您创建了性能会话用于进行“采样”或“检测”分析,则以后始终能将其转换为“并发”模式。一种快速转换方法是使用性能资源管理器中的分析模式菜单。只需选择您需要的模式,就能轻松进入该模式。

  您也可以通过会话“属性”设置来进入所需的模式。在性能资源管理器中指向您的会话,单击右键以显示上下文菜单,然后选择“属性”。“属性”页的“常规”选项卡可用于控制分析会话模式和其他分析参数。

  当您的分析模式设置为“并发”(或“采样”,就此处而言)后,就可以启动您的应用程序(如果您使用的是性能向导,则它已经在您的“目标”列表中;否则您可以手动进行添加),或者连接到已经在运行中的应用程序。性能资源管理器提供了用来执行这些任务的控件,如图 11 所示。


图 11 性能资源管理器的分析控件

  Visual Studio UI 能够自动执行收集分析数据所需的多个步骤。但也可以使用命令行工具来收集分析数据,这种方法对于自动化操作和脚本很有用。

  若要在争用分析模式下启动应用程序,请打开 Visual Studio 命令提示符(它将所有的分析器二进制文件都放到您的路径中,无论是 x86 还是 x64 工具),然后执行以下输入操作:

  VSPerfCmd.exe /start:CONCURRENCY,RESOURCEONLY /output:<您的输出文件>

  VSPerfCmd.exe /launch:<您的应用程序> /args:"<您的应用程序参数>"

  运行您的方案

  VSPerfCmd.exe /detach

  如果您的应用程序已终止,则此步骤不是必需的,但此步骤没有任何危害,因此您可以将其添加到脚本中。

  VSPerfCmd.exe /shutdown

  现在,您可以在 Visual Studio 中打开 YourOutputFile.VSP 进行分析了。

  如果您的应用程序已经在运行中,则可以按照以下步骤将分析器连接到该应用程序:

  VSPerfCmd.exe /start:CONCURRENCY,RESOURCEONLY /output:<您的输出文件>

  VSPerfCmd.exe /attach:<PID 或进程名称>

  运行您的方案

  VSPerfCmd.exe /detach

  VSPerfCmd.exe /shutdown

  有关可用命令行选项的更详细说明,请访问以下网址:msdn.microsoft.com/library/bb385768(VS.100)。

  您可以利用多种 Visual Studio 视图来深入检查所收集的数据。有些视图会显示应用程序生命周期的整体概况,而另一些视图则专门显示特定的争用,您可以使用您认为最有用的视图。

  当您对分析结果进行分析时,可以借助超链接、双击或上下文菜单在不同视图之间切换,也可以通过下拉菜单直接切换到任何可用的视图。图 12 简要介绍了每种视图。

  图 12 分析视图

视图 说明
摘要 “摘要”信息为您的调查提供了一个良好的起点。这是您看到的第一个视图。当分析会话结束且结果文件准备就绪之后,它会自动打开。
调用树 一个聚集了所有争用堆的调用树。您可以在此看到是哪些堆导致了争用。
模块 一个模块列表,这些模块所包含的每个函数都导致了一个争用。每个模块都包含相关函数的列表以及检测到的争用次数。
调用方/被调用方 包含三个面板的视图,这些面板分别显示函数 F、调用 F 的所有函数,以及 F 调用的所有函数(当然,仅限导致争用的调用)。
函数 在任何争用堆上检测到的所有函数及其相关数据的列表。
源代码中的函数行。
资源详细信息 关于特定资源(例如锁)的详细信息,将显示应用程序生命周期内被该资源阻止的所有线程。
线程详细信息 关于特定线程的详细信息,将显示阻止该线程的所有资源(例如锁)。
争用 与调用树视图相似,但在此处,调用树是按争用资源划分的。也就是说,此视图将显示一组调用树,每个树分别包含被某种特定资源阻止的堆。
标记 自动和手动记录的标记列表,其中每个标记都关联了其时间戳以及 Windows 计数器的值。
进程 经检查的进程列表,其中每个进程都具有其线程列表,而每个线程都显示其经历的争用次数以及被阻止的总时间长度。
函数详细信息 关于特定函数的详细信息,包括它调用的函数以及收集的数据。
IP 发生争用的指令指针的列表(即,诸如 EnterCriticalSection、WaitForSingleObject 等之类函数的列表,因为这是争用实际发生的位置)。

借助 Visual Studio 中新的资源争用分析功能,您可以发现由于在代码中使用线程同步而导致的性能问题,并能够在运行时通过更改、减少或消除不必要的同步来提高应用程序的性能。


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3