VisualStudio2010中调试基于任务_第1页
VisualStudio2010中调试基于任务_第2页
VisualStudio2010中调试基于任务_第3页
VisualStudio2010中调试基于任务_第4页
VisualStudio2010中调试基于任务_第5页
已阅读5页,还剩4页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、Visual Studio 2010 中调试基于任务本文来自MSDN杂志Daniel Moth和Stephen Toub下载示例代码如果您询问任何一个CPU或GPU硬件厂商,他们都会告诉您未来属于众核技术时代。处理器内核的速度不会像过去四十年那样以指数速度增长,而新的计算机会配备更多内核。因此,应用程序开发人员过去年复一年所依赖的免费性能改进将不复存在。为了重新获得由越来越好的硬件所提供的免费午餐,并通过新的对性能敏感的特性来增强应用程序性能,您需要借助并行性来发挥多内核的优势。在Visual Studio 2010所提供的Visual C+10和Microsoft.NET Framework

2、 4中,Microsoft将引入新的库和运行时,从而显著简化在基本代码中表达并行性的过程,同时还支持使用新工具对并行应用程序进行性能分析和调试。在本文中,您将了解Visual Studio 2010中的调试支持,其中很多内容都以基于任务的编程模型为中心进行讲述。基于任务编程的需要将并行性融入应用程序是为了利用多个内核。单一的顺序工作一次仅在一个内核上运行。要使应用程序使用多个内核,需要让多项工作启用多个线程,以便对该工作进行并行处理。这样,假如只有一项工作,那么要想通过多核执行实现并行加速,就需要将该单一工作划分为可并发运行的多个单元。最简单的方案是静态划分:将工作划分为固定数目、固定大小的单

3、元。您当然不想针对执行该工作的每种硬件配置都编写代码,并且,随着应用程序在更大型和性能更佳的计算机上运行,预先确定固定数目的单元将会抑制应用程序的扩展性。相反,您可根据计算机的具体情况,在运行时动态选择单元数目。例如,您可按一个内核处理一个单元对工作进行划分。这样,如果所有单元所需的处理时间相等,并且每个单元使用一个线程,您就能够使计算机满负荷工作。但是,这种方法仍然差强人意。在实际环境中划分工作负荷时,很少能够保证每个单元都占用同样的处理时间,尤其在您将一些外部因素(如可能在计算机上同时运行并消耗部分计算机资源的其他工作负荷)考虑在内时。在这种情况下,每个内核一个单元的划分最终可能会造成不均

4、匀的工作分配:某些线程将先于其他线程完成其单元的处理,从而造成负荷不平衡,而某些内核在其他内核完成时处于空闲状态。为解决这一问题,您需要对工作进行深度划分,将工作负荷划分为可行的最小单元,以便计算机的所有资源都能参与工作负荷的处理,直到处理完成。如果执行一个工作单元引起零开销,则刚刚提出的解决方法将较为理想,但很少有实际环境中的操作会引起零开销。根据过去的经验,线程一直是用于执行这样一个工作单元的机制:为每个工作单元创建一个线程,让其执行,然后终止该线程。遗憾的是,线程相对来说是重型的,以这种方式使用它们所产生的开销可能会禁止我们所讲的深度划分类型。您所需要的是一种用于执行这些已划分的单元以将

5、开销降到最低程度的更轻型机制-一种可使您更放心地进行深度划分的机制。通过这种方法,您不是为每个单元创建一个线程,而是利用一个计划程序来计划在它所管理的线程上执行的各个单元,从而在保持单元数目尽可能少的同时仍能确保实现最大吞吐量。我们刚刚介绍的是一个线程池,它将线程管理成本在计划给它的所有工作项中分摊,从而将与单个工作项关联的开销降到最低。在Windows中,可通过从Kernel32.dll导出的QueueUserWorkItem函数访问这种线程池。(Windows Vista还引入了新的线程池功能。)在.NET Framework 4中,可通过System.Threading.ThreadPo

6、ol类访问这种线程池。虽然前面提到的API能以相对很小的开销实现分解,但它们主要是针对即发即弃工作。例如,.NET Framework 4ThreadPool类没有提供任何一致的机制用于异常处理、工作取消、等待工作完成、在工作完成时接收通知等等。这些差距是通过.NET Framework 4和Visual C+10中新的API来弥补的,这些新的API设计用于在托管代码和本机代码中进行的基于任务的编程。任务代表这样的工作单元:它们可由一个基础计划程序高效执行,同时在使用和执行控制方面仍提供丰富的功能。在Visual C+10中,这些API是以Concurrency:task_group和Conc

7、urrency:task_handle类型为中心的。在.NET Framework 4中,它们以新的System.Threading.Tasks.Task类为中心。图1并行堆栈窗口图2并行任务窗口Visual Studio当前提供的调试方法软件开发的历史一再证明,编程模型在很大程度上获益于示范调试支持,而Visual Studio 2010就提供了这样的支持,它提供了两个新的调试工具窗口,以帮助完成基于任务的并行编程。但在介绍这些新功能之前,我们先来回顾一下目前在Visual Studio中进行调试的经历以作为铺垫。(在本文的其余部分,我们将出于解释目的而使用基于.NET任务的类型,但所介绍的

8、调试支持也同样适用于本机代码。)在Visual Studio中调试进程的入口点当然就是附加调试器。默认情况下,当您在Visual Studio中打开的一个项目中按F5(相当于选择调试启动调试命令)时,就会发生调试器的附加。通过选择调试附加到进程菜单命令,您还可以手动将调试器附加到一个进程。附加调试器之后,下一步是进入调试器。进入调试器可通过多种方式发生,包括遇到一个用户定义断点、手动中断(通过调试全部中断命令)、通过请求它的进程(例如,在托管代码中,通过对System.Diagnostics.Debugger.Break方法的调用),或者甚至在引发异常时。图3查找素数static void M

9、ain(string args)var primes=from nin Enumerable.Range(1,10000000).AsParallel().AsOrdered().WithMergeOptions(ParallelMergeOptions.NotBuffered)where IsPrime(n)select n;foreach(var prime in primes)Console.Write(prime+,);public static bool IsPrime(int numberToTest)/WARNING:Buggy!/2 is aweird prime:its ev

10、en.Test for it explicitly.if(numberToTest=2)return true;/Anything thats less than 2or thats even is not primeif(numberToTest 2|(numberToTest&1)=0)return false;/Test all odd numbers less than the sqrt of the target number./If the target is divisible by any of them,its not prime./We dont test evens,be

11、cause if the target is divisible/by an even,the target is also even,which we already checked upperBound=(int)Math.Sqrt(numberToTest);for(int i=3;i upperBound;i+=2)if(numberToTest%i)=0)return false;/Its prime!return true;在进程进入调试器之后,应用程序中的所有线程都将暂停:在您继续执行之前,没有代码在该点执行(调试器自身使用的线程除外)。通过执行中的这种暂停,您可

12、检查该时刻应用程序的状态。当检查应用程序状态时,您的头脑中常常会有一个状态画面,并且,您可使用各种调试器窗口来发现预期与现实之间的差别。开发人员在Visual Studio中使用的主要调试窗口是线程窗口、调用堆栈窗口和变量窗口(局部变量、自动变量、监视)。线程窗口显示进程中所有线程的列表,它包含线程ID和线程优先级以及当前线程的指示(一个黄色箭头)等信息。默认情况下,当前线程是在调试器进入进程中时正在执行的线程。关于线程的最重要信息可能是调试器中断它的执行时其执行位置,此信息通过位置列中的调用堆栈帧显示。将光标悬停在该列上可显示出同等重要的调用堆栈-线程到达当前位置之前正在执行的系列或方法调用

13、。调用堆栈窗口显示了当前线程的调用堆栈,该窗口提供了有关该调用堆栈的更加丰富的信息,包括交互机会。若要在调用堆栈窗口中显示另一个线程的调用堆栈,您必须通过在线程窗口中双击该线程以将其激活为当前线程。当前线程的执行所使用的方法(位于调用堆栈的顶端)由一个黄色箭头指示,称为最顶端帧、叶帧或活动堆栈帧。这是您在离开调试器并继续运行应用程序时线程继续执行所使用的方法。默认情况下,活动堆栈帧也是当前堆栈帧。换言之,它是驱动变量检查的方法,我们将在下面对此进行介绍。图4设置条件断点变量窗口用于检查应用程序中变量的值。本地方法的变量通常可在本地变量和自动变量窗口中进行浏览;全局状态(未在方法中声明的变量)可

14、通过添加到监视窗口中进行检查。从Visual Studio 2005开始,越来越多的开发人员在检查状态时都会选择将鼠标指针悬停在有关变量上,然后查看得到的弹出数据提示(可将其视为快速监视窗口的快捷方式)。务必注意,只有在变量处于当前堆栈帧的范围内时才可显示变量的值(如前所述,当前堆栈帧默认为当前线程的活动堆栈帧)。若要检查线程调用堆栈中先前处于范围中的变量,您需要通过在调用堆栈窗口中双击要检查的堆栈帧来更改当前堆栈帧。此时,新的当前堆栈帧由一个绿色弯尾箭头指示(活动堆栈帧保留黄色箭头)。若还想检查另一线程上的变量,您需要在线程窗口中更改当前线程,然后在调用堆栈窗口中当前线程的调用堆栈上切换当前

15、帧。总之,当您在调试器中进入进程时,可非常轻松地检查处于某个线程的执行方法范围内的变量。但是,若要全面了解所有线程的执行位置,您需要分别检查每个线程的调用堆栈,为此需要双击每个线程以将其激活为当前线程,并检查调用堆栈窗口,然后才能在脑海中形成一个整体画面。并且,若要检查各个线程的各个堆栈帧上的变量,还需要两个层次的间接寻址:切换线程,然后切换帧。并行堆栈当应用程序使用更多线程时(随着人们使用具有更多处理资源的计算机,这种情况将变得普遍),您需要能够在单一视图中看到这些线程在任意给定时刻的执行位置。这就是Visual Studio 2010中的并行堆栈工具窗口所提供的功能。为了节约屏幕空间,同时

16、还要指示对并行方案特别重要的方法,对于在线程根处具有共同点的调用堆栈段,该窗口会将其合并到相同的节点中。例如,在图1中,您可在单一视图中看到三个线程的调用堆栈。该图显示了从Main到A再到B的一个线程,并显示了从相同的外部代码开始然后转到A的另外两个线程。其中的一个线程继续转到B,然后转到某个外部代码,另一个线程继续转到C,然后转到某个AnonymousMethod。AnonymousMethod也是活动堆栈帧,它属于当前线程。此窗口支持很多其他功能,如缩放、总览视图、通过标记来过滤线程以及调用堆栈窗口中已提供的大多数功能。图5选择冻结此线程之外的全部线程命令图6堆栈帧的合并如果您的应用程序创

17、建的是任务而不是线程,则可以切换到以任务为中心的视图。在此视图中,将省略没有执行任务的线程的调用堆栈。另外,还将删减线程的调用堆栈以表示任务的实际调用堆栈,也就是说,一个单线程调用堆栈可能包含两个或三个您想分开并单独查看的任务。通过并行堆栈窗口的一个特殊功能,您可将图表固定在一个方法上,并清晰观察该方法上下文的调用方和被调用方。图7带有依赖关系的基于任务的代码static void Main(string args)/WARNING:Buggy!var task1a=Task.Factory.StartNew(Step1a);var task1b=Task.Factory.StartNew(S

18、tep1b);var task1c=Task.Factory.StartNew(Step1c);Task.WaitAll(task1a,task1b,task1c);var task2a=Task.Factory.StartNew(Step2a);var task2b=Task.Factory.StartNew(Step2b);var task2c=Task.Factory.StartNew(Step2c);Task.WaitAll(task1a,task1b,task1c);var task3a=Task.Factory.StartNew(Step3a);var task3b=Task.Fa

19、ctory.StartNew(Step3b);var task3c=Task.Factory.StartNew(Step3c);Task.WaitAll(task3a,task3b,task3c);并行任务除了可在并行堆栈窗口中查看任务的实际调用堆栈之外,另外一个新的调试器窗口还显示了有关任务的其他信息,包括任务ID、分配给任务的线程、当前位置以及创建时传递给任务的入口点(委托)。称为并行任务窗口的这个窗口具有与线程窗口类似的功能,如指示当前任务(运行在当前线程上的最顶端任务)、能够切换当前任务、对任务进行标记以及冻结和解冻线程。图8使用并行任务查找依赖关系问题状态列对开发人员的价值或许是最大

20、的。通过状态列中提供的信息,您可区分正在运行的任务、正在等待的任务(位于另一个任务或同步基元上)以及发生死锁的任务(等待任务的一种特殊形式,工具针对该任务检测到循环等待链)。并行任务窗口还显示计划任务,即尚未运行但正处于某个队列中等待由线程执行的任务。图2显示了一个这样的示例。并行任务和并行堆栈窗口上的详细信息,请参阅博客张贴/Blog/labels/ParallelComputing.html和在/dd 554943(VS.100).aspx MSDN文档。图9死锁代码static void Main(string args)i

21、nt transfersCompleted=0;Watchdog.BreakIfRepeats()=transfersCompleted,500);BankAccount a=new BankAccountBalance=1000;BankAccount b=new BankAccountBalance=1000;while(true)Parallel.Invoke()=Transfer(a,b,100),()=Transfer(b,a,100);transfersCompleted+=2;class BankAccountpublic int Balance;static void Tran

22、sfer(BankAccount one,BankAccount two,int amount)lock(one)/WARNING:Buggy!lock(two)one.Balance-=amount;two.Balance+=amount;查找Bug了解新工具功能的最佳途径就是在操作中观察其行为。为此,我们创建了几个包含Bug的代码段,并将使用新工具窗口来查找代码中的基本错误。单步执行首先考虑图3中显示的代码。此代码的目标是输出1和10,000,000之间的素数,并以并行方式进行。(并行化支持由并行LINQ提供;有关更多信息,请参见/pfxteam and msd

23、/dd 460688(VS.100).aspx。)IsPrime的实现包含Bug,通过运行该代码并查看输出的前几个数字可以了解这一点:2,3,5,7,9,11,13,15,17,19,23,25,.这些数字大多数为素数,但9、15和25不是。如果这是一个单线程应用程序,您可能就很容易逐步检查该代码,以找出结果不准确的原因。但是,当在一个多线程程序中执行单步执行时(例如,选择调试单步执行),该程序中的任何线程都符合单步执行的条件。这意味着随着单步执行的进行,您可能会在线程之间跳转,这使得对控制流以及关于程序中当前位置的诊断信息的理解更加困难。为了帮助解决这个问题,您

24、可利用调试器的几个功能。第一个功能是设置条件断点。如图4所示,您可设置一个断点(在本例中,断点位于IsPrime方法的第一行上)并指示调试器仅在满足特定条件时介入-在本例中,在计算出不准确的素数之一时介入。我们本可将调试器设置为在遇到这些值之一时介入(而不是遇到它们中的任何值时介入),但我们无法做出有关PLINQ将以什么顺序在后台计算值的假设。相反,我们告知调试器寻找这些值中的任何一个值,以便将它执行中断前的等待时间缩到最短。在调试器介入之后,我们想告知它仅单步执行当前线程。为此,我们可以利用调试器冻结和解冻线程的能力,并规定冻结的线程在解冻之前将不运行。通过新的并行任务窗口,很容易找到应允许

25、继续执行的线程(寻找黄色箭头图标)并冻结所有其他线程(通过上下文菜单),如图5所示。将不相关的线程冻结之后,现在就可以单步执行含有Bug的IsPrime。通过调试numberToTest=25,我们很容易看到存在的错误:循环应在其测试中包含upperBound值,而此值当前被排除在外,原因是该循环使用小于运算符而不是小于或等于运算符。这里,25的平方根是5,25可被5整除,但将不对5进行测试,这样就错误地将25归为素数。并行堆栈窗口还提供一个非常好的整合视图,用于显示在中断时程序中所发生的事情。图6显示了应用程序在再次运行之后的当前状态,这一次将使用调试器的全部中断功能显式介入。PLINQ正在

26、多个任务中执行IsPrime,所有这些任务的numberToTest的值都可在弹出窗口中看到,此时显示任务1正在处理numberToTest=8431901,而任务2正在处理numberToTest=8431607。依赖关系问题图7中的代码显示了并行应用程序中一个常见模式的实例。此代码分出可能并行运行的多个操作(step1a、step1b、step1c,它们都是窗体void StepXx()的方法),然后加入到其中。随后,该应用程序通过代码再次分支,由于存在对操作的副作用(如将数据写到某些共享数组中)的依赖关系,该代码要求前面的操作已完成。遗憾的是,此代码包含一个Bug,编写它的开发人员将看到

27、第三组任务计算出一些不准确的结果。这意味着,即使这位开发人员正在等待前面所有任务的完成,还是存在差错,并非前面的所有计算都已实际完成其结果。为了调试该代码,开发人员在最后的WaitAll调用上设置一个断点,然后使用并行任务窗口查看程序的当前状态,如图8所示。并行任务窗口明确显示,即使已对Step 3的任务进行计划,Step2c的任务仍在运行。对第二个Task.WaitAll调用的检查揭示了原因:由于键入错误,正在等待task1a、task1b和task1c,而没有等待task2的相应操作。死锁图9提供了一种死锁情况的原型示例,这种死锁是因没有注意锁定次序而发生的。主代码在银行帐户间连续进行资金

28、划转。Transfer方法具有线程安全性,以便可从多个线程同时对它进行调用。这样,它就会从内部锁定传递给它的BankAccount对象,为此只需先锁定第一个对象,然后再锁定第二个对象。遗憾的是,如此代码运行结果所示,这种行为可能会导致死锁。最终,调试器在发现没有划转继续进行时介入。(介入是使用代码执行的,该代码若发现在一段特定时间之后没有新的划转完成,就会发出Debugger.Break。此代码包含在随本文提供的代码下载中。)图10有关并行任务中死锁的信息图11显示死锁的并行堆栈图12并行堆栈中的方法视图当您在调试器中工作时,立即会看到一个表明存在死锁的图形表示,如图10所示。该图还显示,在等

29、待死锁状态上悬停指针将提供有关正在等待的确切内容以及哪个线程正在占据受保护资源的详细信息。查看线程分配列,您会看到任务2正在等待由任务1占据的一个资源,如果您将指针悬停在任务1上,您将看到相反的情况。也可从并行堆栈工具窗口得知此信息。图11显示了并行堆栈中的任务视图,它突出显示出存在两个任务,每个任务都在对Monitor.Enter的一个调用中被阻止(因图9中的lock语句)。图12展示了并行堆栈窗口中的方法视图(通过相应工具栏按钮访问)。通过重点查看Transfer方法,很容易可以看到Transfer中目前有两个任务,它们都已转到对Monitor.Enter的调用上。将指针悬停在该框上将提供有关两个任务的死锁状态的详细信息。图13创建锁保护static void Main(string args)/WARNING:Buggy!object obj=new object();Enumerable.Range(1,10).Select(i=v

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论