NET并行计算技术基础_第1页
NET并行计算技术基础_第2页
NET并行计算技术基础_第3页
NET并行计算技术基础_第4页
NET并行计算技术基础_第5页
免费预览已结束,剩余46页可下载查看

下载本文档

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

文档简介

1、.NET并行计算技术基础开篇语:这是一个系列文章,系统介绍.NET4.0中引入的并行扩展(包括任务并行库TPL和PLINQ)开发技术。本系列文章中所有示例代码在VS2010BETAI+Windows7RC下调试通过。本系列文章的主要内容来自是本人将在电子工业出版社出版的新作中的一章,出于技术共享与交流目的发布,期望业界对并行计算有丰富经验的工程师指出疏漏,也希望网友提出宝贵建议。作者金旭亮拥有本系列文章所有版权,未经本人许可,不允许将此序列文章及相关示例用于商业目的,除此之外,在网络上出于知识共享目的的复制转贴不受限制。反馈方式:在我的两个技术博客上直接回贴:1 2 发邮件至:JinXuLia

2、ng2009.9.9随着多核CPU的普及和互联网的迅速发展,计算已经进入并行的时代,这种并行计算有两种主要的形式,一种着眼于充分挖掘单台计算机的硬件潜力,通常以多线程协作的方式完成指定的工作任务;另一种着眼于利用互联的计算机所共同拥有的计算能力,将一个工作任务分发到多台计算机上同时处理,通过多台计算机的相互协作完成单台计算机所无法完成的工作任务。第一种计算形式在过去一直都是使用线程来实现的,而在.NET4.0中,又在线程的基础上向软件工程师提供了一个“并行扩展(ParallelExtensions)”,从一个更高的抽象层次简化多线程应用程序的开发,这也是本章要介绍的主要内容。第二种计算形式依赖

3、于多台计算机的相互协作,本质上是一种分布式的软件系统,在.NET平台上,WCF是开发这类型软件系统的强大工具。本书第9篇的相关章节将介绍WCF技术。19.1并行计算概述BruceSterling11的一段名言:人们在谈论到计算机技术的飞速进步时,经常引用美国著名科幻小说作家Iusedtothinkthatcyberspacewasfiftyyearsaway.WhatIthoughtwasfiftyyearsaway,wasonlytenyearsaway.AndwhatIthoughtwastenyearsaway.itwasalreadyhere.Ijustwasn'tawareo

4、fityet.”译为中文,这段话的大致含义是:我曾经认为网际空间(cyberspace,指互联网这个虚拟的空间)至少要有50年才会影响到我们。那些我认为要50年之后才会影响我们的东西,往往只需要10年就成为现实,。而那些我认为10年之后才会影响我们的东西,现在已经在那儿摆着了,而我甚至还没有意识到它已成为现实!”并行计算就是这样的一个例子,它静悄悄地走来,却于静寞中引发了一场软件技术的变革,当人们开始意识到并行计算对日常生活的影响时,才突然发现,原来并行计算已无所不在。11/wiki/Bruce_Sterling有对BruceSterling本人及其

5、作品的介绍。19.1.1 并行计算是计算的未来从集成电路渗透到计算机领域开始,CPU的计算能力一直在持续地增长。其中有一个著名的“摩尔定律”描述了这一发展趋势。趣闻轶事:“摩尔定律”的提出者是Intel公司的创始人之一GordonE.Moore,1965年他发表了一篇论文,在论文中他针对集成电路技术说过这么一段话:“使用最低成本设计出来的元件具复杂性每年大约增加一倍。可以确信,即便不是有所加快,在短期内这一增长率也会继续保持。从长远的观点来看,这一增长率应该会略有变动。尽管没有充分的理由来证明,(但我认为)这一增长率至少在未来十年内几乎维持为一个常数。这意味着到1975年时,以最小的成本制作出

6、来的单个集成电路将拥有65000个元件,而我相信这么庞大的电路将可以被集成到一块小小的芯片中”。这就是“摩尔定律”的原型。1975年,GordonE.Moore修改了“定律”,将其中的“每年”改为“每两年”。而他的另外一名同事,在“摩尔定律”的基础上又提出了一个“修订版”的“摩尔定律”,新版定律指出,每18个月,集成电路的性能将提升一倍。尽管摩尔本人坚持:“我从没说过集成电路中的元件数量每18个月增加一倍”,但他的声音很快被淹没了,“18个月”作为集成电路的“发展周期”,开始以“摩尔定律”广为人知。从1965年至今,虽然期间有许多人都曾说:摩尔定律已不再有效,但事实却证明这一定律与集成电路技术

7、发展的实际情况相当吻合。到底摩尔定律能用多少年,现在谁也无法下这个结论,还是只能交给时间。但不管怎样,从“摩尔定律”上面人们还是能看到计算机科学家那种深邃的洞察力和让人佩服的预见能力。对于传统的(非并行)应用程序,往往会随着硬件运行速度的提升而运行得更快。举个例子,许多早期的跑在286/386/486上的一些动作类和射击类游戏,如果不作修改地运行于现在的高性能CPU上,其运行速度可能会快到没有人再能玩这些游戏了。这就是计算机硬件技术的发展给软件所带来的直接好处:代码不用或仅略加修改,程序就拥有更快的运行速度。然而,这种免费的午餐是无法永远提供的,其原因在于CPU速度无法无限提升。因为现有的CP

8、U技术需要受到物理法则的限制:当晶体管“瘦身”到很小时,由于控制电流的晶体管门(transistorgate)以及氧化栅极(gateoxide)距离将非常贴近,因此,将发生电子漂移现象(electronsdrift)。如果发生这种情况,晶体管会失去可靠性,原因是晶体管在这种条件下无法有效控制电子的运动,从而难于让其能维持稳定的“1”或“0”状态。为了能突破这一物理限制而继续提升CPU的计算能力,CPU走向了另一个条路一一多核,将多个CPU处理核心集成为单一的整体,称其为“多核CPU。这样一下,虽然每个CPU处理核心的主频都没有提高,但通过将计算任务合理地分配在多个CPU处理核心上运行,我们同样

9、能达到加快CPU处理性能的目的。双核CPU是第一个得到广泛应用的产品,当前几乎所有的新购置的个人电脑都配置了双核CPU而Intel宣布2009年将为台式机和笔记本电脑生产四核CPU新四核CPU基于一个代号为“Nehalem”的微架构)。简言之:如果不对CPU技术作颠覆性的变革,那么,基于现有技术架构的CPU已基本走完了“通过提升主频而获取更高性能”之路,必须走向多核之路,其特点是:(1)每个“核”的主频都不算高(2)在设计CPU架构时,针对并行的工作负载进行了专门优化。CPU勺“多核”发展趋势对软件技术有重大影响。因为CPU的多核化要想真正地发挥效益,必须依赖于精心设计的软件,这种软件本身必须

10、具备将工作任务划分为多个可并行执行的子任务的能力,在操作系统(或特定的软件运行平台)的支持下,将这些任务分配给计算机所配备的多个CPU处理核心并发执行,并且能够在这些子任务运行结束后,将这些结果组合起来,得到最终的处理结果。这就是拥有“并行计算”能力的软件系统。开发“并行计算”软件系统,对软件工程师提出了新的挑战,因为开发并行程序要比申行程序难得多。软件工程师必须转换思路,在开发新的软件系统时,从一开始就考虑到并行计算的特点,并对此进行特定的设计,才能充分利用现有硬件基础设施的强大处理能力,开发出高性能与高可靠性的软件系统,这是一个软件技术发展的必然趋势,也是任何一名软件工程师们都必须认真面对

11、的挑战!19.1.2 并行计算的基本原理并行计算的基本思想并不复杂,那就是将一个复杂的工作任务进行分解,然后,在多个处理器上同时执行这些分解后的子任务。首先,我们先谈谈多核CPU中的“核”。1CPU与“核”现在人们常说:最近我买了一台双核笔记本电脑。这里面的“核(corp)”指代什么,它与CPU是什么关系?还有,人们还常说到“处理器(processor”,貌似这三个都是指同样的东西。下面我们就来讨论一下这些概念的含义,弄清楚它们到底指代什么。学过物理的人都知道,现代计算机是构造于集成电路技术基础之上,任何一个集成电路,最底层的也是最小的工作单元是一个个的晶体管,多个晶体管构成逻辑模块(logi

12、cblock),一组逻辑模块进而构成功能单元(functionalunit),例如可完成基本算术运算功能的ALU(ArithmeticLogicUnit,算术逻辑单元)就是功能单元的一个例子。多个功能单元组合起来,就构成了微处理器(micro-processor,微处理器最广为人知的名字就是CPU(CentralProcessingUnjt中央处理单元)。最终生产出来的微处理器实际上是一块物理基片(die),经过封装之后就叫作处理器(processor。那么什么是“核(core)”呢?核是微处理器中所有可用的功能单元的集合。那些直接参与指令执行的功能单元的集合称为“执行核(executionc

13、ore”。很明显,每个微处理器必定要有一个执行核,否则,它无法执行任何指令,也就不能称其为微处理器了。(激处理弱'执行核最外层Cache前端总线接口单元系统总线或前端总线现在就可很清楚“多核”的含义了。所谓多核,其实就是在同一块物理基片上集成两个以上的“执行核”,从而使处理器具备并行执行指令的能力。多核CPU,就是指拥有多个执行核的微处理器,基片封装好之后,就称为“多核处理器”。再区分一下另两个概念:多处理器与多核处理器系统。如前所述,多核处理器系统是指计算机中的处理器中包容了多个执行核,通常这种计算机只包容有一个多核处理器,否则,此系统应归入“多处理器”系统。多处理器系统是指由多个处

14、理器构成的计算机系统,每个处理器自身可以是单核或多核的。简单地说:多处理器系统可以看作是一个主板上具有多个处理器插槽的计算机系统。从应用软件开发者角度来看,多处理器与多核处理器系统其实是一样的,可以统一地将其看成是计算机中拥有多个可供使用的CPU下面,我们再来区分一下另外两个很容易弄混的概念一一“并发”与“并行”。2并发与并行“并行(Parallelism”是指多个工作任务在拥有多核cpu(或多个单核cpu的多处理器计算机上同时执行。在这些工作任务运行的过程中,除非有任务提前结束或者延迟启动,否则,在任一时间点总有两个以上的工作任务同时运行。简单地说:只要是“同时”运行的,就可以称之为是“并行

15、”的。图19-1展示了并行执行的三个工作任务在三个处理器上同时运行的情形,图中黑色的长条表示任务的执行过程。“并发(Concurrent”,指“宏观”上计算机可以同时执行多个不相关的工作任务(是“并行”的),但在“微观”角度来看,这些工作任务并不是始终都在运行,每个工作任务都呈现出“走走停停”这种相互交替的状态。这里的“宏观”,是指从应用程序开发者层次上看的,时间尺度较大,而“微观”则是指从操作系统的线程管理角度和计算机硬件工程师层次上看的,时间尺度很小。图19-2展示了3个工作任务在单个处理器上交替执行的情形。图19-2中,可以看到通过轮流使用单个处理器,尽管在任何时刻都只有一个工作任务在运

16、行,但在一个比较长的时间间隔内,所有的工作任务都在“并行”运行中。由此可见,“并发”的好处之一就是使有限的处理器资源可以“并行”运行超过处理器个数的多个工作任务。正是由于“并发”有这样的好处,所以在操作系统级别,不管计算机本身是单核系统还是多核系统,都是采用微观上的“并发”来实现宏观上的“并行”,绝不允许一个工作任务长时间地独占某个处理器直到其运行结束。再详细解释一下如何利用“并发”实现“并行”。在单核计算机系统中,操作系统通过“微观”上让每个线程分时使用处理器实现“宏观”上工作任务的“并行”执行(因为工作任务具体是由线程负责完成的),而在多核计算机系统中,尽管在物理架构上直接支持“并行”,允

17、许同时运行多个线程,但由于大多数情况下负责执行工作任务的线程数往往大于可获取的处理器个数,所以,操作系统仍是使用同样的分时调度策略为线程分配处理器。这时,同一个线程有可能在不同的时间段内在不同的处理器上运行。图19-3展示了属于同一工作任务的多个线程在4个处理器上“跳越”执行的情形。理解“并行”与“并发”的区别,是我们开发“并行程序”的第一步上一部分介绍了CPU与核”以及并行”和并发”的区别,这一部分我们将进一步介绍并行计算的性能衡量与并行计算系统的大致分类,为后面介绍.NET4.0的并行计算打下基础。3如何衡量并行计算的性能提升?之所以要研究并行计算,其目的是获得更好的性能。一个软件系统的性

18、能,通常使用两个指标来进行衡量:(1) 响应速度(Responsiveness):用户向软件系统提交一个工作任务,软件系统要花费多长的时间才能处理完毕并将结果通知用户?(2) 吞吐率(Throughput):在单位时间问隔内软件系统最多可以处理多少个工作任务?并行计算的优势就在于它可以缩短系统完成单个工作任务的时间和提升系统的吞吐率。那么,我们怎样定量衡量因为“并行”而带来的系统性能提升?很明显,这里所说的“提升”是有参照物的,对于并行算法,最直观的参照物就是完成同样功能的申行算法。人们使用“加速系数(speedupfactory)ii”来定量衡量一个并行算法的好坏:V使用单处理器执行最佳串行

19、算法所需的时间.-使用4处理器执行并行算法所需丽丽加速系数越大,说明性能提升越明显。我们感兴趣的是:在p个处理器上执行并行算法,到底比在单个处理器上执行用行算法理论上能快多少?如果增加处理器的个数,加速系数能否进一步增大?这种增大有没有一个限制?针对这个问题,并行计算理论中有一个著名的“阿姆达尔定律(Amdahl'slaw)”回答了这个问题。鉴于在实际中“100%纯粹”的并行算法很少,此定律假设算法应同时包容串行执行和并行执行两部分,其中,串行执行的部分占整个算法的比例为f,并且假设并行执行部分除了计算外并无其他开销,则在p个处理器上执行此算法,其加速系数为:从这个公式可以得到一个有趣

20、的结论。试着让p8,则上述公式变为:这说明:在算法中申行部分的比例不变的情况下,不算你使用多少个处理器,加速系数最大也超不过1/f例如,假设某算法中有20%的串行算法,则不管后面你如何加速,最大加速系数超不过5(1/0.2=5)o实际情况可能更糟,因为使用并行算法必然会有其它一开销,比如并行算法都是使用线程来执行的,因此,线程的上下文切换就必须耗用系统资源,考虎到这一点,修正后的“阿姆达尔定律”变为:】-Df+M3)其中H(p)代表算法并行部分在单个处理器上执行所带来的系统开销,不同的算法H(p)的值也不一样。这说明,如果系统开销非常大,那么采用多线程并行执行一个算法,不但没有加速,反而会降低

21、了程序的执行性能。依据“阿姆达尔定律”得出的这个结论令人沮丧!但由此也可以得到一个重要的结论:如果希望使用并行计算来提升程序的性能,那么应尽可能地减少程序中用行代码的比例。扩充阅读:“乐观”的Gustafson定律“阿姆达尔定律”的结论让人沮丧,但到了20世纪80年代晚期,SandiM家实验室的科学家们在对具有1024个处理器的超立方体结构上观察到了3个实际应用程序随着处理器的增加发生线性加速的现象,科学家JohnL.Gustafson基于此实验数据在1988年提出了一个新的计算加速系数的公式:%I=p+Q_p),与“阿姆达尔定律”一样,S(p)(弋表加速系统,p代表处理器数量,f代表算法中申

22、行部分所占的比例。Gustafson定律说明在许多实际的应用程序中得到接近线性的加速效果是可能的。“阿姆达尔定律”的问题出在它的前提过于理想化。因为并行算法通常能处理比用行算法更大规模的问题,即使算法仍然存在着申行部分,但由于问题规模的不断扩大,往往会导致算法中申行部分所占比例的持续减少。Gustafson定律又点燃了人们继续研制集成更多处理器(或集成更多执行核的处理器)的计算机系统的热情。可以预测,未来的计算机将集成更多的处理器,当前双核个人电脑的普及只不过是一个开始罢了。4实际测量并行程序的性能不管是“悲观”的阿姆达尔定律还是“乐观”的Gustafson定律都只是一种理论上的结果,在真实的

23、运行环境中,影响程序性能的因素有许多,仅靠几个简单的公式并不能真正计算出一个真实的“并行”程序比相应的“串行”版本到底快了多少。最可靠的方法还是实地测试,在同样的软件和硬件环境中,先后运行程序的“并行”和“串行”两个版本,比对它们的执行时间,就知道结果了。在.NET程序中,我们可以使用StopWatch类来测试算法的性能。以下是使用StopWatch测试算法运行时间的代码框架:Stopwatchsw=newStopwatch();sw.Start();/启动计时/编写代码执行被测试的算法sw.Stop();/停止计时/获取算法执行时间|longUsedTime=sw.ElapsedM川isec

24、onds;/.另外,调用StopWatch对象的Reset()方法可以重置计时值,再接着调用Start()方法就可以开始一个新的计时。5并行计算系统的大致分类有两种主要的并行计算系统类型。一种称为“共享存储器(SharedMemory)的多处理器”系统,其思想很简单,所有处理器都可以访问同一个主存储器,从而允许运行在多个处理器上的各个线程可以通过主存储器方便地共享信息。基于共享存储器的并行计算系统发展已有许多年,人们也为开发运行于这种平台的软件提供了不少工具,其中得到广泛应用的是OpenMP,它可以帮助程序员使用Fortran(或C/C+)语言高效地开发多线程并行计算程序。C睇口VisualB

25、asic编译器没有遵循OpenMP标准,但通过给.NET基类库添加并行扩展(ParallelExtensions),可以使用比OpenMP更方便的方法设计并行计算程序。扩充阅读:OpenMP简介针对共享存储器的并行计算系统,为了简化软件开发工作,人们设计了一个OpenMP标准,通过给代码中添加特定的编译指令,让实现了此标准编译器自动地帮助我们生成并行指令代码。例如,以下是一个典型的循环代码:for(inti=0;i<100000;i+)/,给上述代码添加一个特殊的编译指令#pragmaompparallelforfor(inti=0;i<100000;i+)(/)遵循OpenMP标

26、准开发的编译器“认识”此指令,在编译时会自动生成相关的指令让此代码可以并行执行。OpenMP的实现机制决定要创建的线程个数,以及如何能够尽善尽美地管理这些线程。开发人员只需告诉OpenMP哪个循环应该以多线程方式执行,至于那些为了利用并行性而进行的线程创建、初始化、线程同步等工作都可以交给OpenMP编译器来完成。OpenMP的标准形成于1997年,支持使用Fortran>C/C+语言编写的程序。另一种称为“消息传递的多处理机”系统,其特点是往往由多台相互连接的计算机构成一个并行计算平台,计算机之间通过“消息”的传递协同工作。基于消息传递构造的并行计算系统本质上是一个分布式软件系统,由于

27、互联网的无孔不入,利用互联的计算机实现并行计算是当前一个非常活跃的领域。在微软平台之上,使用WCF可以很方便地开发这种类型的软件系统。本书第9篇详细介绍WCF。在前面两讲中,基本上介绍完了并行计算基础理论与相关概念,学习不是目的,应用才是。因此,本讲将介绍一个并行计算的例子,并对.NET4.0的并行扩展作一个总体的介绍。19.1.3并行计算所带来的挑战与串行执行的程序相比,开发并行程序需要软件工程师具备一个“多线程”的大脑。我们先来看一个引例,初步体会一下如何使用.NET4.0所提供的任务并行库设计并行程序。1并行计算引例请读者仔细查看一下本节示例程序SequentialvsParalled的

28、源码。此程序完成了一个非常典型的数据处理工作:递增一个整数数组的每个元素值。示例程序将数组大小设定为1000000,然后对数组中的每个元素进行100次操作,每次操作都将元素值加1,因此,完成整个数据处理工作需要108次操作。以下是串行代码:/依次给一个数组中指定部分的元素执行OperationCounterPerDataltem次操作staticvoidIncreaseNumberInSquence(intarr,intstartlndex,intcounter)(for(inti=0;i<counter;i+)for(intj=0;j<OperationCounterPerDat

29、aItem;j+)arrstartIndex+i+;上述代码在笔者的双核笔记本电脑上执行时花费了776毫秒。现在,使用.NET4.0所提供的任务并行库让上述操作并行执行:/将任务划分为TaskCount个子任务,然后并行执行staticvoidIncreaseNumberInParallel(intarr)(intcounter=DataSize/TaskCount;|Parallel.For(0,TaskCount,i=>(intstartIndex=i*counter;IncreaseNumberInSquence(arr,startIndex,counter););1测试结果为41

30、9毫秒,并行加速系数约为1.85。再改算法,将对每个元素的每个操作设定为一个任务,然后再并行执行:staticvoidIncreaseNumberInParallel2(intarr)(/为每个数据项创建一个任务Parallel.For(0,arr.Length,i=>(Parallel.F|5r(0,OperationCounterPerDataItem=>arri+););测试结果为10057毫秒,并行加速系数为0.08,比串行算法慢多了!2并行计算带来的复杂性上面所介绍的例子非常清晰地展示出并行程序设计的特殊性,并不是“并行”总比“串行”快的,到底怎样才能获得最大的并行加速系

31、数,需要仔细地设计并行算法,并且应该在多个典型的软硬件环境中进行对比测试,最终才能得到理想的并行设计方案。开发并行程序的关键在于要找到一个合适的任务分解方案,并行总要付出一定的代价,比如线程同步、线程通讯、同步缓冲数据等都是开发并行程序必须认真考虑的问题。下表对比了并行程序与串行程序的主要差别:项目串行程序并行程序程序行为特性可以预期的,相同运行环境下总可以得到相同的结果如果没有提供特定的同步手段,则程序执行的结果无法预期内存访问独占访问内存单元,数据可靠有可能因多线程同时存取同一内存单元而引发数据存取错误锁不需要必须为共享资源加锁死锁不可能出现可能出现,需要仔细考虑程序中可能出现的种种情况予

32、以避免测试使用代码覆盖的测试方法可以检测出绝大多数BUG由于多个线程同时并行,仅使用代码覆盖的测试方法无法检测出程序中隐藏的BUG,并行程序的测试变得很复杂调试相对简单,可以随时停止程序运行,单步跟踪定位到每条语句和每个变量的值由于多个线程同时运行,当你暂停一个线程进行调试时,其他线程可能还在运行中,因此无法保证调试环境的一致性,并行程序的调试非常困难。正因为并行程序开发、测试和调试都比串行程序要困难,所以一般都是先编写程序的串行版本,等其工作正常之后再将其升级替换为并行版本。3何时使用并行计算”?根据前面的介绍,读者一定对“并行计算”有了个总体的认识,由于“并行”需要付出代价,因此,不是所有

33、的程序都需要转换为并行的,当要处理的数据量很大,或者要执行的数据处理任务繁重,并且这些任务本身就可以分解为互不相关的子任务时,使用并行计算是合适的。对于哪些规模较小的数据处理任务,比如你要编写一个“通讯簿”小程序来保存和检索好友信息,就不必考虑并行处理了,因为要处理数据量不会很大,串行算法的性能就可以满足需求,还用“并行处理”就显得是“牛刀杀鸡”。除了增加程序开发难度之外没有什么好处。19.2.NET4.0中的并行计算组件由于并行计算是将一个工作任务进行分解以并发执行,因此,任何一个支持并行计算的软件开发与运行平台都必须解决这些并发执行的子任务之间的相互协作问题,比如:一个子任务需要等待其它子

34、任务的完成,多个子任务完成之后才允许执行下一个子任务(即所谓fork-join),一个子任务结束后自动启动多个下级子任务的执行允许一个任务中途取消,.NET4.0通过对已有的基类库进行扩充和增强(图19-7),满足了上述需求。System,ThreadingFrtensionsUJL门III:.3IiJUnifiedCanoellatiDnMod由如图19-7所示,.NET4.0给“System.Threading”命名空间增加了一些新白类(,比如在第17章介绍过的Barrier等几个新的线程同步类),同时对部分已有类也进行了调整和优化。另外,针对中途取消线程或作务执行这一实际开发中非常普遍的

35、需求,提供了一个统一取消模型(本书第16章介绍了此模型)。最大的变化是.NET为基类库提供了多个与并行计算密切相关的类,并将它们统一称之为“并行扩展(ParallelExtensions”。WindowsOShrsb如图19-8所示,NET4.0“并行扩展”的主要包括以下几个部分:1 并行语言集成查询(PLINQ,ParallelLanguageIntegratedQuery),这是.NET3.0引入的LINQtoObject(本书第24章介绍)的换代“产品”,让查询操作可以并行执行。2任务并彳亍库(TPL,TaskParallelLibrary):将开发并行程序的抽象级别从“线程(threa

36、d)”提升到“任务(Task)”,只需规定好计算机要执行的任务,然后由.NET去管理线程的创建和同步等问题。3同步的数据结构(CDSCoordinationDataStructures):包括一组线程安全的常用数据结构,比如线程安全的队列、堆栈等,在并行程序中访问这些数据结构,可以不需要显式地使用lock。4任务调度器(TaskScheduler):负责任务的创建、执行、暂停等管理工作。5线程?t!l:.NET4.0对原有的托管线程池功能进行了大幅度的增强,通过给其集成一个任务调度器,线程池中的线程可以高效地并行执行各种任务。上述五个组成部分当中,PLINQ是建立在TPL之上的,而TaskSc

37、heduler是并行计算的核心,是一个Runtime,它与线程池相集成,负责将任务分派给线程池中的各个线程执行。下面几个小节中,我们就整个.NET4.0并行扩展中与软件工程师关联最紧密的两个主要组成部分一一任务并行库TPL和并行语言集成查询PLINQ的内部机理进行剖析,然后在此基础上详细介绍使用TPL和PLINQ开发并行程序的基本技巧。19.1让一切并行”一一任务并行库原理及应用19.3.1 任务并行库简介任务并彳T库(TPL:TaskParallelLibrary)是.NET4.0为帮助软件工程师开发并行程序而提供的一组类,位于System.Threading和System.Threadin

38、g.Tasks这两个命名空间中,驻留在3个.NET核心程序集mscorlib.dll、System.dll和System.Core.dll里。使用这些类,可以让软件工程师在开发并行程序时,将精力更关注于问题本身,而不是诸如线程的创建、取消和同步等繁琐的技术细节。使用TPL开发并行程序,考虑的着眼点是“任务(task)”而非“线程”。一个任务是一个Task类的实例,它代表某个需要计算机执行的数据处理工作,其特殊之处在于:在TPL中,任务通常代表一个可以被计算机并行执行的工作。任务可以由任何一个线程执行,特定的任务与特定的线程之间没有绑定关系。在目前的版本中,TPL使用.NET线程池中的线程来执行

39、任务。负责将任务“分派”到线程的工作则“任务调度器(TaskScheduler)”负责。任务调度器集成于线程池中。我们在前面介绍并行计算基本原理时,曾经介绍过OpenMP,通过在Fortran或C/C+代码中添加特定的编译标记,实现了OpenMP标准的编译器会自动地生成相应的并行代码。然而,TPL采用了另一种实现方式,它自行是作为.NET平台的一个有机组成部分而出现的,并不对编译器提出特殊要求,当应用程序使用TPL编写并行程序时,所有代码会被直接编译为IL指令,然后由CLR负责执行之,整个过程完全等同于标准的.NET应用程序。换言之,对于应用软件开发工程师而言,使用TPL开发并行程序,在编程方

40、式上没有任务变化,只不过是编程时多了几个类可用,并且处理数据时需要使用并行算法。提示:之所以微软在设计.NET4.0并行扩展的时候放弃了类似于OpenMP的方式,是因为.NET平台本身是跨语言的,如果象OpenMP那样,就不得不对所有的.NET编程语言设定特定的编译指令,并且需要修改现有的各种语言编译器,这无疑是不明智的一个决定。另外,针对并行程序中令人头痛的异常处理问题,TPL提供了一个增强了的.NET异常处理机制,并且在VisualStudio中集成了相应的调试工具。扩充阅读:使用VisualStudio2010调试并行程序Threads窗口查看VisualStudio2010对并行程序的

41、调试提供了强大的手段,给程序设计好断点以后,可以使用当前程序的所有线程:在图19-9中双击某行,可以让指定的线程成为当前“激活”的“被调试”的线程。另外,ParallelTasks窗口展示了当前程序所运行的所有任务:在ParallelStacks窗口中,则可以直观地看到每个线程的调用堆栈:有关VisualStudio2010调试器的使用方法,请查询MSDN。本书不再赘述。19.3.2 从线程到任务TPL开发并行程序(图19-12)在又tTPL有了基本的了解之后,我们以一个实例来介绍如何使用1示例简介示例项目CalculateVarianceOfPopulation完成以下任务:测试一批数据的总

42、体方差。依据数理统计理论,可以使用以下公式计算方差:很明显,要完成计算数据总体方差的任务,必须完成以下的工作:(1)计算出所有数据的平均值,这很简单,直接求数据的和然后除以数据个数就行了。(2)计算所有数与平均值的差值的平方,然后求和(3)将第(2)步求出的各除以数据个数,得到总体方差。分析一下,在上述3个子任务中,第(2)步是最有可能并行执行的。我们可以将整个数据分成几组,然后对每组数据并行执行处理任务。下面简要介绍一下示例程序的技术要点,完整代码可以在配套光盘上找到。2直接使用线程实现并行处理在示例程序中,测试数据是随机生成的,放在一个double类型的数组中,其大小由常量DataSize

43、确定。示例程序是一个windows应用程序,为了保证程序可以及时地响应用户操作,均采用多线程方式在后台执行计算任务,为此设计了一个跨线程安全显示信息的函数:privatevoidShowInfo(stringInfo)if(InvokeRequired)Action<string>del=(str)=>rtfInfo.AppendText(str);this.BeginInvoke(del,Info);)elsertflnfo.AppendText(Info);)注意上面用到了Control.InvokeRequired属性用于判断是否跨线程访问RichTextBox控件。串

44、行程序没什么好说的,示例程序将其封装为一个CalculateVarianceInSequence()函数,直接调用就行了。有趣的是如何使用线程来并行处理。常量ThreadCount用于定义并行执行上述第(2)个任务的线程数,示例中将其设置为4,因此,在程序运行时,有4个线程同时计算“每个数据与总体平均值的差值的平方和”。这是一个典型的线程同步问题。我们使用一个窗体的成员变量SquareSumUsedByThread保存计算结果,由于有4个线程要访问它,因此必须给其加上一把锁。这里有一个需要注意的地方,为了提升程序性能,这把锁”锁定的对象不能是主窗体对象,更不能是主窗体类型,而是一个专用于互斥的

45、对象。为此,在主窗体中我添加了以下变量:privateobjectSquareSumLockObject=newobject();而在线程函数中这样访问它:/lock(SquareSumLockObject)SquareSumUsedByThread+=sum;)/这是一个很重要的多线程开发技巧,读者需要注意。另外,工作线程在执行计算任务时需要知道一些信息:它负责处理整个数组中“哪块”区域?这可以通过它要处理的数据的起始索引和要处理的数据个数确定。总体数据的平均值,这个值在算法前一步使用串行算法计算得到的。读者一看到这,应该马上意识到这是一个典型的“将数据从外界传送到线程中”问题,可以使用本书

46、第16章介绍过的相关编程技巧来解决。在本示例中,定义了一个ThreadArgu辅助类用于封装这些信息。由此得到线程函数的代码框架:privatevoidCalculateSquareSumInParallelWithThread(objectThreadArguObject)ThreadArguargu=ThreadArguObjectasThreadArgu;/(代码略)17章介绍过的CountdownEvent对象来等待另外,由于有4个工作线程执行计算任务,因此,我们可以使用第这4个线程的工作结束。如果读者掌握了前几章的内容,那么在上面介绍的基础之上,您完全可以不看示例代码自行编出这个程序

47、,这是一个很好的编程练习。下一讲介绍如何使用TPL来完成同样的工作,示例的源码也将在下一讲给出3使用任务并行库实现并行处理上面介绍了基于线程编码实现并行处理的技术要点,可以看到还是比较繁琐的。但通过使用.NET4.0的并行库,可以简化开发工作。我们略微详细一点地介绍一下示例程序中是如何使用任务并行库实现并行计算的。其中的一个关键函数是ForRange(1S数,先来看看它的声明:publicstaticParallelLoopResultForRange(intfromInclusive,inttoExclusive,Action<int,int>body)/代码略前两个参数代表要计

48、算的数据在数组中的起始和结束索引,第3个参数是一个Action委托,它引用一个将被并行执行的处理函数。在并行计算程序中,任务的分解方式是一个需要仔细考虑的问题,有一种常用的方案就是依据本机所包容的处理器个数来决定并行处理的任务数,可以直接调用.NET基类库中的类来获取这一信息。intnumberOfPartitions=System.Environment.ProcessorCount;确定了要分解的任务数,就可以算出每个子任务负责处理的数据项数:/获取要计算的数据范围intrange=toExclusive-fromInclusive;/计算出每个并行任务要计算的数据个数intstride=

49、range/numberOfPartitions;if(range=0)numberOfPartitions=0;现在到了关键的部分,我们不是使用线程来执行每个子任务,而是直接调用.NET4.0任务并行库中的Parallel类来完成这一个工作:returnParallel.For(0,numberOfPartitions,i=>intstart=i*stride;intend=(i=numberOfPartitions-1)?toExclusive:start+stride;body(start,end);|);Parallel.For()H一个静态方法,它的第3个参数是类型为Actio

50、n<int>的委托,在这里,我们直接使用Lambda表达式来将一个函数直接“内联”作为For()方法的参数。For()方法有一个ParallelLoopResult类型的返回值,可以通过此返回值的IsCompleted属性了解For()方法启动的所有任务是否运行结束。交叉链接:如果读者不熟悉Action和Func等系统预定义委托的含义及用法,请参看本书第12章微软的创委托揭秘Lambda表达式则是.NET3.0引入的特性,与LINQ技术关联紧密,相关内容请参看本书第24章统一的数据访问模式:LINQ。在示例程序中,通过以下代码调用上面定义好的ForRange(方法启动并行计算:/代

51、码略doublemean=CalcuateMeanInSequence(Data);/计算平均值/启动并行计算ForRange(0,Data.Length,(start,end)=>doublesum=0;doubletemp=0;for(inti=start;i<end;i+)temp=Datai-mean;sum+=temp*temp;|)/保存结果lock(SquareSumLockObject)SquareSumUsedByThread+=sum;);|);/代码略注意对ForRange(方法调用的第3个参数又是一个Lambda表达式。建议读者一定要花费一点时问去习惯阅读和

52、编写Lambda表达式,今后您一定会在许多场合看到类似的语法现象。4小结在本小节中,我们通过一个示例程序对比了“顺序算法”,“使用线程的并行算法”和“基于TPL的并行算法”三种编程方式。很明显,顺序算法最简单,最易于理解,直接使用线程实现并行计算的,需要编写的代码最多,调试起来也麻烦,而使用TPL可以不需要直接地与线程打交道,除了语法上略微“奇怪”和“别扭”一些(呵呵,看多了就习惯了)之外,还是比较简洁的。从执行时间上看,顺序算法最快,TPL次之,而使用线程的最慢。必须指出,程序运行的快慢与许多因素有关,比如计算机硬件配置,是否采用了优化算法,以及操作系统平台等,上述性能比较结果是在一台双核笔

53、记本+Windows7下得到的,读者的结果有可能不样。从性能比较的结果来看,并行计算并非总具有性能优势,这也提醒我们要注意并行计算的应用场(1)每个数据项要执行的处理工作量很大,需要耗费较多的时间(2)要处理的数据集合很大。很明显,对于求方差这样一个任务而言,对于每个数据项要执行的操作实在太简单了,只不过是加加减减和乘乘除除,因此,用行算法的性能更优。另外,尽管TPL极大地简化了并行程序的开发门槛,在很多情况下不需要直接地与线程打交道,但仍然需要开发者掌握多线程开发的基础知识与基本技能:比如示例使用lock来互斥地访问多线程共享的变量SquareSumUsedByThread所以,多线程技术是

54、开发并行计算程序的基础。21所谓“相互独立”的方法,指这些方法未共享任何资源,对执行顺序也没有任何要求。2使用Parallel.For并行访问数据(partition)另一种非常常见的并行场景是并行循环,例如:上述循环中,要等待前一个循环结束之后,下一个循环才开始执行。如果将其改为以下形式:Parallel.For(0,100,(i)=>DoWork(i);则这些循环就可以并行执行了。注意:并行执行的循环其执行顺序是“乱”的,每次运行都可能会有不同的执行顺序。示例项目ParallelFor展示了这一特性。Parallel.For还有几个重载的形式,涉及到中途取消操作和异常捕获的问题,我们

55、将在后面的章节中介绍。一个有意思的问题是,如果在一段顺序执行的程序代码中有一句调用了Parallel.For启动一个并行循环,如下所示:语句1;Parallel.For(,);语句2;/,其他语句那么,语句2(及后继语句)是在Parallel.For执行完后才执行呢还是与Parallel.For并行执行?在MSDN中并没有明确地回答这个问题。而我经过实验,发现要等到Parallel.For执行完后才会执行语句2(及后继语句)。技术探险之旅:Parallel.For的内部实现对于好奇的读者,不妨使用Reflector工具去深入探究一下Parallel.For的实现代码,虽然这些代码比较复杂,但不

56、难发现以下代码框架:/,(代码略)ParallelForReplicatingTaskrootTask=null;rootTask=newParallelForReplicatingTask(,);rootTask.RunSynchronously(parallelOptions.EffectiveTaskScheduler);/,(代码略)rootTask.Wait();/,(代码略)rootTask.Dispose();|/,(代码略)其中ParallelForReplicatingTas斐是TPL内部定义的一个类,其基类是Task。从上述代码框架中,不难明白Parallel.For的工作原理:TPL在Parallel.ForTj法内部创建了一个任务对象rootTask,然后调用此对象的RunSynchronously方法以“同步”方式执行并行循环,注意,别被这里的单词“Synchronously(中文译为“同步地”)给欺骗了,此方法绝不是串行执行的。因为此方法接收一个参数,此参数引用一个任务调度器对象,由此调度器对象将任务进行分解,交由线程池中的线程执行,这是实现并行循环的关键!任务交给线程池中白

温馨提示

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

评论

0/150

提交评论