现在的位置: 首页 > 综合 > 正文

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

2013年12月12日 ⁄ 综合 ⁄ 共 5504字 ⁄ 字号 评论关闭

 

19.1


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



        
LINQ


的出现对于
.NET

平台而言是一件大事,它使用一种统一的模式查询数据,并且可以紧密地与具体编程语言直接集成。
LINQ

语句的编写方式是“动态组合”和“递归”的,这与函数式编程语言(如
F#

)类似,这种编写方式的优点在于代码量小,通过动态组合一些典型的查询运算符,可以实现相当复杂的数据处理逻辑,而同样的功能如果采用传统的编码方式实现,将耗费不少的力气写代码。

        
.NET 4.0


引入的
PLINQ


LINQ

的“升级换代”技术,它允许以并行方式执行
LINQ

查询。

        


使用
PLINQ

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

        


在本节中,我们先介绍
PLINQ

与其他技术的关系,然后介绍如何编写
PLINQ

查询,最后,剖析
PLINQ

内部的工作原理。

 

交叉链接:

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

 

19.4.1 PLINQ

概述


        
PLINQ


主要用于并行执行数据查询,而它本身又是
.NET 4.0

所引入的并行扩展的有机组成部分,因此,它与
LINQ


TPL

都有着密切的联系。

        
LINQ


,是英文词组“
Language-Integrated Query



的缩写,中文译为“语言集成的查询”,分为
LINQ to Object


LINQ to SQL


LINQ to XML


LINQ to DataSet

等几个有机组成部分。

        


在目前的版本中,
PLINQ

只实现了
LINQ to Object

的并行执行,换句话说,
PLINQ

实现了对“内存”中的数据进行并行查询。如果数据来自于数据库或文件,您需要将这些数据加载到内存中才能使用
PLINQ


        


标准的
LINQ

查询运算符是由“
System.Linq.Enumerable

”类所封装的扩展方法实现的,类似地,
PLINQ

也为所有标准的
LINQ

查询运算符(如
where


select

等)提供了并行版本,这些并行的
PLINQ

查询运算符实现为
.NET 4.0

新增的“
System.Linq.ParallelEnumerable

”类的扩展方法。

        

交叉链接:

      

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

 

        



LINQ

查询转换为
PLINQ

非常简单,在许多情况下只需简单地添加一个
AsParallel

子句就行了,例如,以下代码将把整数集合中的偶数挑出来:

 

       
 
//


创建一个
100

个元素的整数集合,保存从
1


100

的整数
.

           
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


运行得更慢!

如果在某个地方做不到,它就采用串行方式执行。

        


在真实的应用程序中,要确定到底性能有无提升,请直接运行
LINQ


PLINQ

的两个版本进行对比测试以决定取舍。

        


一般来说,对于小数据量的数据集而言,优先选择
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

查询内部所使用的数据分区策略和并行算法”,它对于应用软件开发工程师而言是不可控的因素,但却对性能影响很大。此处只是提醒读者在编码时需要注意这些细节。

抱歉!评论已关闭.