一起谈.NET技术,探秘.NET 4和Visual Studio 2010中的多核利用

  如果你想利用多核机器的强大计算能力,你需要使用PLINQ(并行LINQ),任务并行库(Task Parallel Library,TPL)和Visual Studio2010中的新功能创建应用程序。

  以前,如果你创建的多线程应用程序有BUG,那要跟踪起来是很麻烦的,但现在情况完全变了,感谢微软为我们带来了Microsoft Parallel Extensions for .NET(.NET并行扩展),它在.NET框架线程模型上提供了一个抽象层。

  并行扩展遵循微软在COM应用程序中建立的事务管理和在数据访问领域建立的实体框架和LINQ模型,它试图通过给.NET框架中的复杂过程建立高级支持,以便将先进的技术带给大众,随着多核处理器的普及,开发人员渴望他们的应用程序可以利用所有处理器核心的计算能力。

  你可以通过并行LINQ(PLINQ)和任务并行库(Task Parallel Library,TPL)使用并行扩展的功能,它们都允许你为单核和多核计算机写一套代码,依靠.NET框架,最大限度利用代码执行平台的计算能力,并防止自行创建多线程应用程序时常见的陷阱。

  PLINQ扩展了LINQ查询,它将单个查询分解成多个并行运行的子查询,TPL允许你创建并行运行的循环,而不是一个接一个地运行,虽然PLINQ的声明语法使创建并行进程更加简单,但一般情况下,面向TPL的操作比PLINQ查询更轻量级。

  许多时候,选择TPL还是PLINQ只是一种生活方式,如果喜欢并行循环,而不是并行查询,那么设计一个TPL解决方案比设计一个PLINQ解决方案更容易。

  PLINQ简介

  对于商业应用程序,只要LINQ查询涉及到多个子查询时,PLINQ就像金子一样发光,如果你要连接本地数据库某张表中的行和另一个远程数据库某张表中的行,PLINQ将非常有用,在这种情况下,LINQ必须在每个数据源上独立运行子查询,然后调和结果,PLINQ将会把这些子查询分配给多个处理器核心,这些子查询就可以同时执行。实际上,你使用的处理器周期不是少了,而是更多了,当然好处就是你可以更早得到结果,请阅读“并行处理不会让你的应用程序变得更快”了解更多关于多线程应用程序的行为。

  并行处理不会让你的应用程序变得更快

  关于多线程应用程序最常见的一个误解是,应用程序线程越多,运行速度就越快,多启动一个线程并不会导致Windows给你的应用程序更多的处理周期,它只是把这些周期划分给更多线程了,实际上,在单处理器计算机上,开启多线程只会让你的应用程序变得更慢。

  多线程只是让你的应用程序响应更快,但它仍然要等待其它阻塞任务完成先,不过在等待期间,你可以利用多线程应用程序的特点让其它线程做一些别的事情。在单核机器上,如果线程未被阻塞,多个线程只能相互争夺有限的处理周期。

  多核处理器改变了这种状况,在多核环境中,你可以让Windows给你的应用程序分配更多的处理周期,你不需要阻塞线程,所有线程都在它们自己的核心上执行。并行扩展提供了编程结构,允许你告诉.NET框架应用程序那些部分可以并行执行。

  即使在多核机器上,PLINQ也并不总是并行的查询,有两个原因,一是你的应用程序并行运行不会总是更快,第二个原因是,即使你有一个抽象层管理你的线程,在并行处理时总会出现脚步不一致的情况,PLINQ会检查一些不安全的条件,如果检测到就不会进行并行查询。我会指出PLINQ不会检查的问题和条件,但使用PLINQ出了问题只有你自己负责处理。

  处理PLINQ

  调用PLINQ很简单,只需要在你的数据源中添加AsParallel扩展,下面是一个从本地Northwind数据库连接远程Northwind数据库,根据客户(customer)信息查询订单(Orders)的示例:


1. Dim ords As System.Linq.ParallelQuery(Of ParallelExtensions.Order)
2. ords = From c In le.Customers.AsParallel Join o In re.Orders.AsParallel
3. On c.CustomerID Equals o.CustomerID
4. Where c.CustomerID = "ALFKI"
5. Select o

  因为两个数据源都标记了AsParallel(在连接时,如果一个数据源使用了AsParallel,另一个也必须使用),因此将会使用PLINQ。

  和普通的LINQ查询一样,PLINQ查询使用延迟处理,即等到你要真正使用数据时,它才会开始检索,这意味着即使LINQ查询声明了是并行的,在你要处理结果前不会发生并行处理,除非使用下面这样的代码块:


1. For Each ord As Order In ords
2. ord.RequiredDate.Value.AddDays(2)
3. Next

  在后台,PLINQ将使用一个线程执行For …Each循环中的代码,而其它线程可能被用来执行子查询,最大可以使用64个线程,请阅读“并行控制”材料了解这种行为的更多信息。

  并行控制

  本文认为并行LINQ(PLINQ)总是好的,例如,首先选择是否要并行运行,然后决定如何将多个子查询分配给多个线程,你可以使用With*扩展控制PLINQ的行为。

  在使用调试工具的时候,你会发现PLINQ不是并行执行查询的,你可以传递ParallelExecutionMode .ForceParallelism值给WithExecutionMode方法让其强制并行执行查询。


1. ords = From o In le.Orders.AsParallel.
2. WithExecutionMode(ParallelExecutionMode.ForceParallelism)

  如果你想指定线程的数量(例如,你想让一或多个处理核心闲置),你可以使用WithDegreeOfParallelism方法,下面的代码示例将线程数限制为3。


1. ords = From o In le.Orders.AsParallel.
2. WithDegreeOfParallelism(3)

  你也可以使用cancellation结束处理过程,首先创建一个CancellationTokenSource对象,然后将其传递给WithCancellation扩展。


1. Dim ctx As New System.Threading.CancellationTokenSource
2. ords = From o In le.Orders.AsParallel.
3. WithCancellation(ctx.Token)
4. Where o.RequiredDate > Now
5. Select o
6.
7. For Each ord As Order In ords
8. totFreight += ord.Freight
9. If totFreight > FreightChargeLimit Then
10. ctx.Cancel()
11. End If
12. Next

  如果你正在处理For…Each循环中的PLINQ查询结果,调用cancellation会自动退出循环。

  如果在一个订单(Order)上的处理过程不和另一个订单上的处理过程共享状态,可以使用ForAll循环进一步提高响应,ForAll可以用于支持Lambda表达式的PLINQ查询结果集,它和For…Each循环不一样,For…Each只在程序的主线程中执行的,而传递给ForAll方法的操作是在PLINQ查询产生的独立查询线程上执行的。


1. ords.ForAll(Sub(ord)
2. ord.RequiredDate.Value.AddDays(2)
3. End Sub)

  此外,For…Each循环是在它自己的线程中串行执行的,而ForAll中的代码是在检索订单的线程上并行执行的。

  管理顺序

  虽然和SQL类似,但PLINQ不保证顺序,PLINQ子查询返回结果的顺序依赖于各个线程不可预知的响应时间,例如下面这个查询是为了获得将要先发货的五个订单。


ords = From o In re.Orders.AsParallel
Where o.RequiredDate > Now
Select o
Take (5)

图 1 PLINQ给TPL中的功能添加查询分析和标准查询操作,TPL提供管理操作系统底层线程需要的基本的结构和调度

  如果不保证顺序,我将获得一个随机的订单(Orders)数据集,它们可能是(也可能不是)应该先发货的五个订单,为了确保得到前五个订单,我需要在查询中增加一个Order By子句,按照日期对查询结果进行排序,当然这样就会丢掉PLINQ的一些好处。

  因为结果来自多个线程,难免不会出现异常,PLINQ不能明白“上一条”和“下一条”的概念,如果在你的循环中刚好要用到下一条项目的值时,完全有可能会遭遇错误的处理,为了让订单中的项目按照原始数据源中的顺序处理,你需要在查询中增加AsOrdered扩展。

  例如,如果我想将低于某一运费的所有订单打包到一起处理,我可能会写下面这样一个循环:


1. For Each ord As Order In ords
2. totFreight += ord.Freight
3. If totFreight > FreightChargeLimit Then
4. Exit For
5. End If
6. shipOrders.Add(ord)
7. Next

  由于并行处理返回的项目顺序不可预知,因此进入批处理的订单可能是随机的,为了保证按照原始数据源中的顺序处理返回的结果,我必须给数据源加上AsOrdered扩展。


1. ords = From o In re.Orders.AsParallel.AsOrdered
2. Where o.RequiredDate > Now
3. Select o

  TPL(任务并行库)介绍

  如果你的处理不是由LINQ查询驱动的,你可以使用借鉴了PLINQ的TPL技术,从根本上看,TPL让你创建可并行执行的循环,如果你的计算机是四核的,一个循环可能用1/3的时间就完成了。

  如果不使用TPL,你可能会像下面这样处理Orders集合中的所有元素:


1. For Each o As Order In le.Orders
2. o.RequiredDate.Value.AddDays(2)
3. Next

  如果使用TPL,你调用Parallel类的ForEach方法,通过Lambda表达式来处理集合中的项目:


1. System.Threading.Tasks.Parallel.ForEach(
2. le.Orders, Sub(o)
3. o.RequiredDate.Value.AddDays(2)
4. End Sub)

  通过使用Parallel ForEach,每个方法的实例可以在独立的处理器上同时处理,如果每个操作需要1毫秒,并且有足够的处理器存在,所有的订单就可以在1毫秒内处理,而不是1毫秒乘以订单数量的时间。

  任何复杂的处理放在Lambda表达式中都会变得很难阅读,因此你要经常想到在你的Lambda表达式中调用下面这样一些方法:


1. System.Threading.Tasks.Parallel.ForEach(
2. le.Orders, Sub(o)
3. ExtendOrders(o)
4. End Sub)
5. ...
6. Sub ExtendOrders(ByVal o As Order)
7. o.RequiredDate.Value.AddDays(2)
8. End Sub

  从本质上讲,TPL将集合中的成员分配给独立的任务,这些任务又被分配到所有处理核心上执行,每个任务完成时释放掉代码,TPL调度器从执行队列中取出另一个任务开始执行,你也可以根据索引值使用For方法创建一个循环。

  当你创建自定义任务时你才会感觉到TPL的强大之处,任务创建好后使用它的Start方法启动,但它更容易使用Task类的静态工厂对象(Factory),它的StartNew方法可以创建并启动任务(Task),你只需要通过一个Lambda表达式就可以使用StartNew方法,如果你的函数要返回一个值,你可以使用Task对象的Generic版本指定返回的类型。

  下面的示例为计算订单总价的Order Detail对象创建并启动了一个Task,Task被添加到一个列表(List)中,后面的代码循环检索List中的结果,如果我需要一个未计算的结果,第二个循环将会暂停,直到Task完成。


1. Dim CalcTask As System.Threading.
2. Tasks.Task(Of Decimal)
3. Dim CalcTasks As New List(Of System.
4. Threading.Tasks.Task(Of Decimal))
5. For Each ord As Order_Detail In
6. le.Order_Details
7. Dim od As Order_Detail = ord
8. CalcTask = System.Threading.
9. Tasks.Task(Of Decimal).
10. Factory.StartNew(Function() CalcValue(od))
11. CalcTasks.Add(CalcTask)
12. Next
13.
14. Dim totResult As Decimal
15. For Each ct As System.Threading.Tasks.Task(Of Decimal) In CalcTasks
16. totResult += ct.Result
17. Next

  如果我足够幸运,在我需要结果前,Task总是先完成,即使不走运,也要比按顺序运行每个Task更早得到结果。

  凡是遇到一个Task的输出要依赖于另一个Task先完成的情况,你可以在Task之间创建依赖或将Task分组,最简单的办法是使用Wait方法,但它会导致你的应用程序停止执行,直到所有Task全部完成。


1. Dim tsks() As System.Threading.Tasks.Task = {
2. Task(Of Decimal).Factory.StartNew(Function()
CalcValue(le.Order_Details(0))),
3. Task(Of Decimal).Factory.StartNew(Function()
CalcValue(le.Order_Details(1)))
4. }
5. System.Threading.Tasks.Task.WaitAll(tsks)

  一个更复杂的方法是使用Task对象的ContinueWith方法,当其它Task完成时,它触发一个Task继续运行。下面的例子启动了多个线程,每个都计算订单明细(Order Detail)的值,但都只有等到订单明细上的其它操作完成后才能执行。


1. For Each ordd As Order_Detail In le.Order_Details
2. Dim od As Order_Detail = ordd
3. Dim adjustedDiscount As New Task(Sub() AdjustDiscount(od))
4. Dim calcedValue As Task(Of Long) =
5. adjustedDiscount.ContinueWith(Of Long)(Function() CalcValue(od))
6. adjustedDiscount.Start
7. Next

图 2 并行堆栈窗口提供了一个可视化视图,显示了当前执行的线程的附加信息

  出错时如何处理

  在多个处理器上同时执行多个线程也会造成异常出现得更频繁,任何线程上一旦发生异常,整个应用程序都将挂起,给AggregateException对象添加的错误处理也会增加,通过这个对象的InnerExceptions属性允许你查看每个线程的异常。


1. Dim Messages As New System.Text.StringBuilder
2. Try
3. 'PLINQ or TPL processing
4. Catch aex As AggregateException
5. For Each ex As Exception In aex.InnerExceptions
6. Messages.Append(ex.Message & "; ")
7. Next
8. End Try

  注意这里没有使用Catch语句,你需要检查InnerExceptions的类型,确定每个线程究竟抛出的是什么异常。

  调试并发线程变得更加有趣,因为异常可能随一个PLINQ查询中的循环出现,解决这个问题可能需要重构PLINQ查询,幸运的是,Visual Studio 2010包括了额外的工具调式并行错误。

  并行堆栈窗口(Parallel Stacks)超越了旧的线程窗口,线程窗口只能提供一个视图,而并行堆栈窗口可以显示所有正在执行的线程,例如,它默认允许你同时查看多个线程的调用堆栈,你可以放大显示内容,也可以过滤只显示指定的线程,更重要的是,如果你使用TPL,你可以切换到基于任务的视图(对应于你代码中的Task对象),或方法视图(显示调用方法的任务),但使用并行任务窗口(Parallel Tasks)可能更有用,因为它围绕Task组织任务,这个窗口不仅显示当前运行的任务,已调度和等待运行的任务也会显示(显示在状态[Status]列),你可以通过检查当前运行的Task是否在等待其它任务,从而确定Task之间的依赖关系。

  在早期的Visual Studio版本中,要一步一步调式多线程程序是一场噩梦,因为调试器要从一个线程中的当前语句跳转到另一个线程的当前语句,并行任务(Parallel Task)允许你冻结或解冻与Task相关的线程,在调试时控制哪一个线程先运行。

  一起使用这两个窗口可以简化并行处理问题的诊断,例如,Visual Studio现在检测到一个死锁时,它会自动打破死锁,当调式器检测到两个或多个Task不能处理时(因为相互都在等待对方释放锁定的对象),Visual Studio将实施冻结处理,就好像你遇到一个断点似的,并行任务窗口将显示每个Task在等待的对象,以及它占有的线程,并行堆栈窗口的方法视图可视化显示了发生死锁时哪个Task调用了哪个方法。

  其它调试功能

  除了这些工具外,Visual Studio还包含了其它几个用于调式并行处理的功能,在遍历你的代码时,当你的鼠标移到一个Task对象上时,弹出一个提示窗口,显示该任务的Id,关联的方法和它当前的状态(如,等待执行)等详细信息,进一步展开该提示,可以看到该Task的属性值,包括它的结果。在观察窗口(Watch)中检查Task的InternalCurrent属性,可以得到当前正在执行的Task的信息,任务调度器(TaskScheduler)的提示展开后可以看到它管理的所有Task。

  合理使用PLINQ,TPL和Visual Studio提供的功能,无论你的应用程序运行在什么计算机上,你都可以利用所有处理器的计算能力。

时间: 2024-10-02 09:01:39

一起谈.NET技术,探秘.NET 4和Visual Studio 2010中的多核利用的相关文章

探秘.NET 4和Visual Studio 2010中的多核利用

如果你想利用多核机器的强大计算能力,你需要使用PLINQ(并行LINQ),任务并行库(Task Parallel Library,TPL)和Visual Studio2010中的新功能创建应用程序. 以前,如果你创建的多线程应用程序有BUG,那要跟踪起来是很麻烦的,但现在情况完全变了,感谢微软为我们带来了Microsoft Parallel Extensions for .NET(.NET并行扩展),它在.NET框架线程模型上提供了一个抽象层. 并行扩展遵循微软在COM应用程序中建立的事务管理和

一起谈.NET技术,我眼中的Visual Studio 2010架构工具

影响架构质量的是构建体系架构的思想.原则.实践与架构师的经验,绝不是工具.即使是最优秀的架构工具,也不可能像倚天宝剑一般--倚天一出,谁与争锋--似乎谁握住了这把利刃,就能够成为武林盟主.架构工具可以改善架构师的工作,却不能替换架构的过程.软件开发过程中,最重要的依旧是人. 我在尝鲜Visual Studio 2010架构工具[i]时,偶然看到一篇文章,用夸张的语言吹捧VS 2010架构工具,认为它是架构师最怕程序员知道的新工具.这让我有感而发,我想起数十年前甚嚣尘上的一个理论,那就是CASE工

一起谈.NET技术,让你的Visual Studio 2010 支持HTML5 和 CSS3

现在的热门话题之一是HTML5 和 CSS3.好的, 它们都很时髦,它们也必然会影响网络开发的未来. 让我们尝尝鲜,花点时间安装设置一下,尽快让Visual Studio2010支持HTML5 和 CSS3 吧. 首先到微软网站上下载 Visual Studio 2010 SP1.双击开始执行VS10sp1-KB983509.exe, 然后等待补丁的下载安装完成. – 由于Visual Studio 2010 一开始并没有支持HTML5, 但是在SP1包中,微软开始加入HTML5的规范. HTM

一起谈.NET技术,浅析Visual Studio 2010中常见Web.Config变换

我最近一直在摆弄Visual Studio 2010 Beta版,我最喜欢的一个新的特点(新特点很多)是新版web.config变换. Web.config变换是启动设定,所以你的每个编译链接设置都有一个设置"delta"(默认的是Debug和Release).当你建立你的解决方案(比如当你发布一个包的时候)你原始的Web.config会根据你的Web.debug.config文件中的设置变换(以debug设置为例). 有很多日志和MSDN可供你参考,但是我想我应该写一个简明扼要的帖子

一起谈.NET技术,Visual Studio 2010中敏捷开发流程模板的应用

本文将会为您讲述如何使用TFS 2010 MSF Agile 5.0流程模板为迭代项目而提供的工作簿.首先,让我们看一看Visual Studio 2010 IDE中的Team Explorer插件: 接下来,我们将会进入到SharePoint project portal中,为大家展示Team Explorer是如何模拟它的结构的.我们首先右击名字是Team Project的标题的那个节点,如下图所示: 这会让我们进入到SharePoint project portal中,如下图所示.注意Te

Visual Studio 2010“.NET技术” 中的代码约定设置

软件约定称为代码约定,通过这一约定可以表示代码正常工作所需的正式条件. 如果方法未按预期收到数据或生成的数据不符合预期的后置条件,代码约定将导致代码引发异常. 有关前置条件和后置条件的概述,您可能需要查看我上个月发表的文章 (msdn.microsoft.com/magazine/gg983479). 代码约定是 .NET Framework 4 的一部分,但同样依赖于 Visual Studio 2010 中的一些功能,例如运行时工具.与 MSBuild 集成以及"项目属性"框中的属

一起谈.NET技术,应用Visual Studio 2010辅助敏捷测试(下)

随着需求的不断变化和迭代的深入,代码库不可避免的会有频繁的签入和签出,此时测试人员一项很重要的任务就是要预防回归问题发生.执行手工测试用例可以帮助我们预防及和发现回归问题,但是它的执行效率太低,无法胜任频繁执行的要求.这时就我们需要考虑采用自动化测试用例完成这项工作.决定是否采用自动化测试是有很多因素决定,其中很重要的一条就是自动测试的收益,下面的公式从概念上解释了如何来计算这个收益,当收益值大于1的时候,实施自动化测试就是合算的:否则,就是不合算的. 图1:计算收益公式 这其中,开发和维护自动

一起谈.NET技术,应用Visual Studio 2010辅助敏捷测试(上)

敏捷软件开发是近些年来比较热门的话题,<敏捷宣言>四条主要精神和十二条基本准则概括了敏捷开发的基本思想.围绕着这些基本概念和思想,产生了一系列的轻量级方法,如:极限编程.测试驱动开发.Scrum.特性驱动开发等.虽然具体名称.过程和侧重点不尽相同,但是相对于非敏捷的开发方法而言,它们都更强调面对面的沟通.团队不同角色之间的紧密协作.频繁交付新的可用的软件版本.紧凑而自我组织型的团队等.敏捷开发只是提供了一个思想和方法论,而要在实际的工程中正确运用它,并真正显现出它的优点和产生实际的效果,这对于

一起谈.NET技术,Visual Studio 2010着力打造云计算平台

在RC版本发布之后,我们看到微软为Visual Studio 2010增加了很多新功能,同时在云计算方面也有很大的改善,随着云计算时代的到来,软件开发模式和商业模型都将进入全面开放组合的新时代.微软云计算平台Windows Azure,这将把微软带入一个崭新的时代. Azure来源于法语,语意为天空一样的湛蓝色,这也正是微软所希望的,把其打造成承载所有云上的应用和服务的蓝天.既然微软一开始就表明了要在云计算领域中一展身手,当然不会让我们失望.在微软的Visual Studio 2010产品中,就