.NET4.0并行计算技术基础(11)

今天终于在MSDN看到,微软将于2009年10月21日向公众开放VS2010 BTEA2的下载,受够了VS2010 BTEA1的不稳定与缓慢的速度,对新版本期望很久了,希望BETA2能够修正BETA1中巨多的BUG,成为一个成熟稳定的开发平台。
今天贴出PLINQ部分的内容,也许等BETA2发布后,我得动手修改我的文章了,不过我估计在基类库方面应该不会有大的变化,而大的变化应该集中于开发工具VS 2010本身,所以,本系列文章中介绍基本技术与原理是不会有什么过时的。
PLINQ部分将分两次贴出。
金旭亮
2009.10.20
=================================================
.NET4.0并行计算技术基础(11)

前几讲的链接:

.NET4.0并行计算技术基础(7)

.NET4.0并行计算技术基础(8)

.NET4.0并行计算技术基础(9)

.NET4.0并行计算技术基础(10)

================================================

19.1让查询执行得更快——Parallel LINQ

LINQ的出现对于.NET平台而言是一件大事,它使用一种统一的模式查询数据,并且可以紧密地与具体编程语言直接集成。LINQ语句的编写方式是“动态组合”和“递归”的,这与函数式编程语言(如F#)类似,这种编写方式的优点在于代码量小,通过动态组合一些典型的查询运算符,可以实现相当复杂的数据处理逻辑,而同样的功能如果采用传统的编码方式实现,将耗费不少的力气写代码。

.NET 4.0引入的PLINQLINQ的“升级换代”技术,它允许以并行方式执行LINQ查询。

使用PLINQ技术的最大好处之一是当计算机处理器个数增加时,不需要修改(或仅需少量修改)源代码,程序性能就可以得到相应的提升。

在本节中,我们先介绍PLINQ与其他技术的关系,然后介绍如何编写PLINQ查询,最后,剖析PLINQ内部的工作原理。

交叉链接:

要学习本节,要求读者掌握了LINQ编程的基本技巧。本书第24章详细介绍LINQ,可供读者参阅

19.4.1 PLINQ概述

PLINQ主要用于并行执行数据查询,而它本身又是.NET 4.0所引入的并行扩展的有机组成部分,因此,它与LINQTPL都有着密切的联系。

LINQ,是英文词组“Language-Integrated Query 的缩写,中文译为“语言集成的查询”,分为LINQ to ObjectLINQ to SQLLINQ to XMLLINQ to DataSet等几个有机组成部分。

在目前的版本中,PLINQ只实现了LINQ to Object的并行执行,换句话说,PLINQ实现了对“内存”中的数据进行并行查询。如果数据来自于数据库或文件,您需要将这些数据加载到内存中才能使用PLINQ

标准的LINQ查询运算符是由“System.Linq.Enumerable”类所封装的扩展方法实现的,类似地,PLINQ也为所有标准的LINQ查询运算符(如whereselect等)提供了并行版本,这些并行的PLINQ查询运算符实现为.NET 4.0新增的“System.Linq.ParallelEnumerable”类的扩展方法。

交叉链接:

本书3.2.4小节介绍了扩展方法,扩展方法在LINQ中有着重要的应用,本书第23章介绍了这方面的内容。

LINQ查询转换为PLINQ非常简单,在许多情况下只需简单地添加一个AsParallel子句就行了,例如,以下代码将把整数集合中的偶数挑出来:

//创建一个100个元素的整数集合,保存从1100的整数.

var source = Enumerable.Range(1, 100);

var evenNums = from num in source.AsParallel()

where num % 2==0

select num;

可以看到,PLINQ查询除了多一个AsParallel子句之外,与标准LINQ的查询并没有什么不同,原有的绝大多数LINQ编程方法仍然继续适用。

.NET语言编译器“看到”一个查询中包含AsParallel子句代码时,它会在编译期间引用System.Concurrency.dll程序集,将相应的标准LINQ查询运算符替换为对ParallelEnumerable类相应静态方法的调用,同时“悄悄地”将查询的返回值修改为相应的并行版本(比如许多PLINQ查询返回一个ParallelQuery<T>类型的数据集合)。由于ParallelQuery<T>派生自IEnumerable<T>,而后者是许多标准LINQ查询运算符的返回数据类型,因此,PLINQ利用多态性保证了它与原有LINQ代码的最大兼容性。

LINQ类似,PLINQ也具有“延迟执行”的特性,只有对查询集合调用foreach迭代、或者调用ToList之类方法时,PLINQ查询才会真正执行。

设计者在设计PLINQ,追求的一个目标是:PLINQ绝不能比它的前辈--LINQ to Object运行得更慢!如果在某个地方做不到,它就采用串行方式执行。

在真实的应用程序中,要确定到底性能有无提升,请直接运行LINQPLINQ的两个版本进行对比测试以决定取舍。

一般来说,对于小数据量的数据集而言,优先选择LINQ而不是PLINQ

提示:

如果需要的话,可以使用AsSequential子句“强制”PLINQ查询采用串行方式执行。甚至可以在同一条查询语句中混用“并行”与“串行”两种模式。

另外,PLINQ在底层使用TPL所提供的基础架构完成所有工作,因此,PLINQ是比“Task”抽象层次更高的编程手段,在实际开发中,只要可能,推荐直接使用PLINQ

总之,在设计并行程序时,推荐按照以下顺序来设计技术解决方案:

基于PLINQ的声明式编程方式à使用Task的直接基于TPL的“任务并行”编程方式à使用线程的基于CLR的“多线程”编程方式

19.4.2 基于PLINQ开发

由于PLINQ建立于LINQ基础之上,实现了所有标准LINQ运算符的并行版本,因此,本节只介绍PLINQ中不同于LINQ的技术特征。读者需要先掌握好编写标准LINQ查询语句的基本技巧,才可以掌握本节介绍的内容。

1 LINQ查询转为并行执行

LINQ查询语句中,在一个可以返回数据集合的子句后面大都可以添加一个AsParallel()AsParallel<T>()子句将其转换为PLINQ语句。

以下是一个例子,从一个整数集合中取出所有的偶数

List<int> lst = new List<int>();

//lst中追加数据,代码略...

var evenNums = from num in lst.AsParallel<int>()

where num%2==0

select num;

上述代码执行时,TPL引擎会自动在后台创建并管理线程,让查询得以并行执行。

然而,在某些情况下,由于并行处理会带来错误的结果,因此必须强制将其转为串行模式,这时,可以调用ParallelEnumerable类的扩展方法AsSequential()达到此目的。

请看示例AsParallelAndAsSequential

示例程序在一个保存了学生考试成绩的数据集合中查找60分以上的学生,并且按照成绩高低排名次。

如果使用PLINQ来完成这个工作,我们可以写出以下代码:

int counter = 0;//计数器

var query =

from student in students.AsParallel()

where student.Score > 60 //分数大于60

orderby student.Score descending //按成绩降序排序

select new //返回学生信息

{

TempID = ++counter, //学生成绩名次

student.Name,

student.Score

};

//输出处理结果,代码略……

请注意上述代码中加了方框的部分,由于上述PLINQ查询是并行执行的,这就是说会有多个线程同时访问counter变量,这就隐含着一个“多线程同时访问共享资源”的问题,而且到底TPL会创建多少个线程,以及这些线程的推进顺序是不可控的,因此,上述代码执行结果是错误的,学生成绩与名次不能正确对应。

如果将上述代码中的AsParallel子句去掉,则结果正确,但却失去了并行执行的任何好处。

如何能在享受PLINQ所带来的性能提升的好处的同时,又能避免因并行执行而得到错误的结果?

其关键在于要分清楚数据处理工作中哪些可以并行执行,哪些必须串行执行,然后,再将其组合起来。

对于这种需要混合并行与串行执行的情况,直接使用PLINQ语句比较困难,通常在这种场景下使用扩展方法实现。

在本例中,可以写出以下代码同时组合“并行执行”与“串行执行”的两种数据处理工作。

var query2 = students.AsParallel() //使用并行查询

.Where(student => student.Score > 60) //分数大于60

.OrderByDescending(stu => stu.Score) //按成绩降序排序

.AsSequential() //强制转换为串行执行

.Select(studentInfo =>

new

{

TempID = ++counter, //名次

studentInfo.Name,

studentInfo.Score

});

上述代码中,按成绩筛选记录是并行执行的,而生成处理结果集合时是顺序执行的。

2 维持数据的顺序

默认情况下,PLINQ查询要处理的数据被认为“顺序无关紧要”,TPL会按照它内置的算法将数据分成几组(称为“数据分区”),然后在这些相互独立的数据分区上并行处理。

然而在某些情况下,数据的顺序是重要的,请看示例项目AsOrdered

示例项目先用随机整数填充了一个List<int>集合对象source,然后,程序找出排在最前面的10个偶数出来,要求保持原有顺序。

以下PLINQ查询完成这个工作。

var parallelQuery = from num in source.AsParallel().AsOrdered()

where num % 2 == 0

select num;

var First10Numbers = parallelQuery.Take(10);

上述查询语句中的AsOrdered()子句强制PLINQ保持原始数据的排列次序。

读者可以试一下,如果去掉AsOrdered()子句,则得到的结果是错误的。

AsOrdered()ParallelEnumerable类提供的静态扩展方法,因此适用于绝大多数数据集合类型。

注意:

AsOrdered()和前面介绍的AsSequential()是不一样的,AsSequential()强制PLINQ查询以串行方式执行,而AsOrdered()仍是并行执行的,只不过并行执行的结果先被缓存起来,然后再按原始数据顺序进行排序,才得到最后的结果。

很明显,给PLINQ查询加上AsOrdered()子句将会影响到程序的性能,因此,尽量避免使用它。

在一些情况下,可以通过修改PLINQ查询的顺序避免使用AsOrdered()子句。例如,假设整数集合中的原始是排好序的,则以下PLINQ查询按顺序取出所有的偶数:

var evenNums = from num in source.AsParallel().AsOrdered()

where num % 2 == 0

select num;

如果对查询操作的顺序进行一下修改,会得到更好的性能:

var evenNums = from num in source.AsParallel()

where num % 2 == 0

orderby num

select num;

当然,我们并不能肯定“修改之后的代码一定比修改前快”,因为这取决于许多因素,特别是“TPL执行PLINQ查询内部所使用的数据分区策略和并行算法”,它对于应用软件开发工程师而言是不可控的因素,但却对性能影响很大。此处只是提醒读者在编码时需要注意这些细节。

================================================

请看《.NET4.0并行计算技术基础(12)

附注:

文章刚贴出,就看到网友的回贴要源码。

呵呵,其实我觉得掌握编程原理与思维方法更重要,所以在正文中只点出技术关键点,觉得不必要贴出全部源码,否则,这本书1000页都不够,因为我几乎针对每个技术关键点都设计并编写了相应的实例。大的实例高达上千行代码。
由于文章还在不断地写作过程中,写一点贴一点,又由于CSDN没有提供随文上传源码的功能,因此我没有随文提供下载链接。
但我会在贴完全部文章后,将所有源码打包放到CSDN下载频道供大家下载。

大家在看完这系列文章后发现有哪些错误,以及对我的写作有何建议,敬请回贴指出,或者直接发到我邮箱。

谢谢大家。

你可能感兴趣的:(.net)