PLINQ -> Parallel Extensions

来源:互联网 发布:乐视网络电视报价 编辑:程序博客网 时间:2024/05/16 17:24

PLINQ改名为Parallel Extensions发布

 

Parallel Extensions最初发布的时候叫做PLINQ或者Parallel LINQ。当时它是一个LINQ Provider,可以自动地将查询并行化。不过现在它的范围已经大大扩张了。

  其中最重要的一个改变是不再局限于查询。他们认识到有些算法无法自然地用LINQ来表达,因此将会有一套强制式的数据并行API。

  例如,有一个用在匿名函数里头的Parallel For语法。不过对于VB用户来说,在不支持多行匿名函数的语言里,新语法看起来没那么清爽。另一个选择是Threading.Tasks命名空间。它为调度操作提供了一个高级的任务管理器。与现在的线程池不同,在这个管理器中任务可以和其他任务关联。因此取消一个任务就可以自动地取消该任务的全部子任务。在Parallel Programming with .NET博客上有更详细的介绍。

  有些被强烈要求的特性还没出现在Parallel Extensions当中。Joe Duffy列出了其中的一些,包括可验证的线程安全(verifiable thread safety)以及自动化的并行性(automatic parallelism)。另一个考虑中的特性是使用图形处理器来完成通用的操作。

  Parallel Extensions CTP版要求.NET 3.5。

 

Parallel Extension

LazyBee

随着双核处理器的逐渐普及标志着多核处理器的时代已经来临。为了适应这种变化,充分利用多核的硬件资源,微软正在研发下一代并行系列框架(Parallel FX)。PLINQ(Parallel LINQ)TPL(Task Parallel Liberary)就是其中的关键组件。同时也在加大力度研发自己的在并行方面应用出色的函数式语言F#(目前版本是1.9.3RC)。目前微软已经将PLINQTPL包含在Parallel Extension中,在200712月,微软推出了Parallel ExtensionCTP版本(社区预览版),你可以通过以下地址http://www.microsoft.com/downloads/details.aspx?FamilyID=e848dc1d-5be3-4941-8705-024bc7f180ba&displaylang=en去下载这个版本。在运行安装文件ParallelExtensions_Dec07CTP.msi之后,在C:/Program Files下将创建Microsoft Parallel Extensions Dec07 CTP目录,在目录中包含文档、示例以及System.Threading.dll文件(安装时已经增加到GAC中了),PLINQTPL就包含在System.Threading.dll文件中.在安装完成之后,你就可以非常方便的使用并行扩展创建你的并行程序。

为了使用PLINQ,你只需要简单的将System.Threading.dll添加到应用库中,然后调用System.Linq.ParallelQuery.AsParallel扩展方法即可,例如我们有这样的LINQ代码片段:

string[] words = new[] { "Welcome", "to", "Beijing" };

(from word in words select Process(word)).ToArray();

我们很容易就可以将其变成PLINQ版本:

string[] words = new[] { "Welcome", "to", "Beijing" };

(from word in words.AsParallel() select Process(word)).ToArray();

 

初识Parallel ExtensionsPLINQ

LazyBee

上一篇我们已经大概介绍了Parallel Extensions的安装和它的大致用途(请参见:初识Parallel Extensions。今天我们就来谈谈平行扩展的关键组件之一PLINQ(Parallel LINQ)。微软对PLINQParallel FX中的定位是:PLINQTPL(Task Parallel Library)的一个高层应用。由于目前微软对TPL研发的时间还比较短,这个社区预览版的TPL版本的质量还是比较低的,而且微软发布这个版本的目的也是为了更好的获得开发社区的反馈信息,为了让PLINQ有更高的质量,所以目前PLINQ还是基于ThreadPool的实现,而不是基于TPLAPI的。不过这只是内部实现不同而已,以后正式发布的时候PLINQ的对外接口的变更应该不会太大。

如何使用PLINQ?

1 添加System.Threading.dll到引用中

2 通过调用System.Linq.ParallelQuery.AsParallel扩展方法,将数据封装到IParallelEnumerable<T>中。

基于声明方式的数据并行性

调用AsParallel扩展方法可使得编译器使用System.Linq.ParallelEnumerable版本的查询运算符,而不是System.Linq.Enumerable的。熟悉LINQ的人都知道,查询表达式在编译时都将转化成对扩展方法的调用。对于LINQ而言,所有的扩展方法都封装在System.Linq.Enumerable静态类中,该类定义的都是针对IEnumerable<T>数据源的扩展方法。而对于PLINQ,所有的扩展方法都封装在System.Linq.ParallelEnumerable静态类中,而且该类针对的都是IParallelEnumerable<T>数据源的扩展方法,是System.Linq.Enumerable静态类扩展方法的镜像,只不过是通过并行方式对查询进行评估IParallelEnumerable<T>接口从IEnumerable<T>接口继承,所以PLINQ也具有LINQ的延迟执行的特点,以及执行foreach

为了让大家清晰知道系统是如何将使用System.Linq.Enumerable版本的查询运算符变成System.Linq.ParallelEnumerable版本的,我们先来看看System.Linq.ParallelQuery.AsParallel方法:

public static IParallelEnumerable<T> AsParallel<T>(IEnumerable<T> source)

很显然这个方法就是将IEnumerable<T>的数据源转化成IParallelEnumerable<T>以使得使用平行版本的运算。这就是平行架构中通过AsParallel的声明来使用并行使用数据的方式,也是PLINQ的编程模型。

所以这种基于声明方式的数据并行性使得从LINQPLINQ的转化非常容易,例如我们有这样的LINQ代码片段:

string[] words = new[] { "Welcome", "to", "Beijing" };

(from word in words select Process(word)).ToArray();

我们很容易就可以将其变成PLINQ版本:

string[] words = new[] { "Welcome", "to", "Beijing" };

(from word in words.AsParallel() select Process(word)).ToArray();

       当然,如果你是通过查询操作(就是直接调用静态扩展函数),而不是使用查询表达式(有时候查询表达式没有提供相应的表达式语句,例如C#3.0中没有提供SkipTake相对应的查询表达式语句,我们只能通过直接调用查询操作函数)的情况下,将LINQ迁移到PLINQ,我们除了要调用AsParallel方法,还需要将直接调用Enumerable的方法改成对ParallelEnumerable的调用,例如:

IEnumerable<T> data = ...;

var q = Enumerable.Select(Enumerable.OrderBy(

Enumerable.Where(data, (x) => p(x)),(x) => k(x)),(x) => f(x));

foreach (var e in q) a(e);

要使用 PLINQ,必须按如下方式重新编写该查询:

IEnumerable<T> data = ...;

var q = ParallelEnumerable.Select(ParallelEnumerable.OrderBy(

                ParallelEnumerable.Where(data.AsParallel(), (x) =>    p(x)),

                                                        (x) => k(x)),(x) => f(x));

foreach (var e in q) a(e);

注意:有些查询运算符是二元的,使用两个IEnumerable<T>作为输入参数(例如Join),最左边数据源的类型决定了使用LINQ还是PLINQ,因此你只需要在第一个数据源上调用AsParallel便能使查询并行查询,例如:

IEnumerable<T> leftData = ..., rightData = ...;

var q = from x in leftData.AsParallel()

        join y in rightData on x.a == y.b

        select f(x, y);

PLINQ查询处理模型

1 管道式(Pipelined Processing)

     该模型是将查询线程(运行查询的线程)和枚举线程(进行迭代输出结果的线程)分开,处理器在有元素可用时就运行枚举将输出应用于foreach循环。也就是说不需要等到所有查询结果完成才进行枚举输出,只要查询结果能产生一个最终输出结果时就会进行枚举输出。简单说就是边查询边输出,这个模型的好处就是允许对输出做更多增量处理,从而减少为了存放结果所需的内存,坏处是由于中间结果需要更多的同步而降低性能。PLINQ缺省的是采用此模型。

2 准动态(stop-and-go Processing

在这种模型下启动枚举的线程会联结所有其他线程来执行查询。在所有查询结果完成之后才进行枚举输出。这种模型的效率比管道式稍微高一些,因为这种模型需要同步的系统开销减少了。在对查询进行ToArray,ToList或者排序聚合操作时,系统将自动转为这种模型处理。因为这些操作都需要产生所有输出。具体在代码中是通过调用IParallelEnumerable接口的GetEnumerator的重载方法并且传递false参数来使用这种模型的,该方法如下:

IEnumerable <T> GetEnumerator(bool usePipelining)

3 反转枚举(Inverted Enumeration)

       该模型会为并行运行的PLINQ提供一个Lambda表达式,集合中的每个元素都运行一次。这是最高效的一种模型,因为它将高成本运算的控制反转给Lambda函数了。但注意的是Lambda函数中不能使用共享状态,否则可能会导致系统崩溃,因为PLINQ不知道如何进行并发同步控制。但有些不同的是,此模型不能简单使用foreach循环,而必须使用特殊的ForAll API.例如:

string[] words = new[] { "Welcome", "to", "Beijing","OK","Hua","Ying","Ni" ,"2008"};

var lazyBeeQuery = from word in words.AsParallel() select word;

lazyBeeQuery.ForAll<string>(word => { Console.WriteLine(word); });

在我的机器上(双核)的输出结果是:

Hua

Welcome

Ying

Ni

2008

to

Beijing

OK

细心的人可能会发现其顺序和数组的顺序不同,这就是PLINQ并行运行的结果,可能在您的机器上可能结果又不同。

同时AsParallel重载方法提供一个参数来控制查询的并行度(就是多少个线程被用于查询),该方法定义如下:

public static IParallelEnumerable<T> AsParallel<T>(
IEnumerable<T> source,int degreeOfParallelism)

 如果你希望在使用管道式处理时有一个单独的线程专门用于枚举输出,你可以将degreeOfParallelism参数赋值为(Enviorment.ProcessCount-1)即可。

输出结果顺序

由于并行的原因输出结果可能和原有的数据在数据源中的顺序不一样,例如:

string[] words = new[] { "Welcome", "to", "Beijing","OK","Hua","Ying","Ni" ,"2008"};

var lazyBeeQuery = from word in words.AsParallel() select word;

foreach (string word in lazyBeeQuery)

{

 Console.WriteLine(word);

}

这时的输出结果可能是:

Welcome

Hua

to

Ying

Beijing

Ni

OK

2008

如果我们希望输出结果和原有的数据在数据源中的顺序保持一致,可以使用AsParallel的带有ParallelQueryOptions.PreserveOrdering参数的重载版本,例如上例中就可以改成如下就可以使输出顺序和原有结构一致:

var lazyBeeQuery = from word in words.AsParallel(ParallelQueryOptions.PreserveOrdering) select word;

注意:1 ParallelQueryOptions.PreserveOrdering参数的使用对ForAll API不起作用(目前是这样,以后不知道是否会做改动)。

      2 使用这个保留顺序的选项会影响查询的性能和扩展能力,因为PLINQ将从逻辑上在末尾增加一个排序操作,而排序是一个无法随处理器数量的增加而显著提高性能的运算符,所以要在必须的时候才用。

并发异常

在顺序执行LINQ的时候,任何异常都会停止后续查询的运行。但在PLINQ中,由于是并行运行的,某一线程产生了异常,系统会尝试尽快终止其他线程的运行,在所有线程关闭之后,产生的所有异常将会放到System.Threading.AggregateException中,你可以通过InnerExceptions属性来得到所有异常只读集合ReadOnlyCollection<Exception>

 

Ed Essey,微软并行计算团队的程序经理,记述了一些针对PLINQ的最新增强功能,很快要发布的.NET 4.0 Beta 1将会包含这些功能。他们涉及到:“With"运算符模式(Operators Pattern)、执行模式( Execution Mode)、取消操作(Cancellation)、部分代码的重构、性能提升。
 
  在Beta 1中完整的PLINQ增强列表是:

  ·With- 运算符模式
  ·执行模式
  ·取消操作
  ·自定义分区
  ·部分代码的重构
  ·合并选项
  ·AsMerged又重新命名为AsSequential
  ·二元运算符现在需要两边都进行AsParallel
  ·性能提升
  ·删除了很少用到的运算符

  “With”运算符模式。有4个新的方法:

  ·e.AsParallel().WithDegreeOfParallelism
  ·e.AsParallel().WithExecutionMode
  ·e.AsParallel().WithCancellation
  ·e.AsParallel().WithMergeOptions

  执行模式。PLINQ把消耗相似资源过程调整为一个LINQ-to-Objects查询,特别地与内存消耗相关的东西。当一个PLINQ调用被认为会消耗太多资源,那么调用就连续地执行而不进行并行处理。是否转换到连续执行,是根据查询的具体情况来决定的。如下的查询就会被连续地执行:

 

·包含编入索引的Select,编入索引的Where,编入索引的SelectMany或者位置不在原始顺序上的ElementAt的查询。索引排序容易受到改变排序规则(如OrderBy)运算符和删除元素(如Where)运算符的影响。

  ·在已经偏离原始顺序的情况下(参看上面的条目),包含Take、TakeWhile、Skip、SkipWhile运算符的查询,。

  ·包含Zip、SequenceEquals的查询,除非其中一个数据源具有初始排序的索引,并且其他数据源是可索引的(比如一个数组或IList<T>)。

 ·包含Concat的查询,除非它是应用到可索引的数据源上。

 ·包含Reverse的查询,除非应用到可索引的数据源上。

  为了强制并行执行,可以这样做:
e.AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism)

  取消操作。并行运算可以如下面例子所示那样被取消: 

var cts = new CancellationTokenSource();
var q
= a.AsParallel().WithCancellation(cts.Token).Where(x=>Filter(x)).Select(x=>DoWork(x);
-- separate thread --
foreach (var e in q) { … }  // Statement 1
-- separate thread --
var l
= q.ToList(); // Statement 2
-- separate thread --
cts.Cancel();
// this will attempt to cancel any in-flight queries,
// including both statements 1 and 2

  自定义分区。Partitioner<TSource>、OrderablePartitioner<TSource>类和Partitioner工厂类提供了对数据如何分区的控制。
 
  部分代码的重构。IParallelEnumerable、IParallelEnumerable<T>和IParallelOrderedEnumerable<T> 接口不再是接口而变为不可被扩展的抽象类了:ParalellQuery、ParalellQuery<TSource>和OrderedParallelQuery<TSource>。原因是,一开始他们就不会被扩展。
 
  合并选项。“从AsMerged中移出了ParallelMergeOptions的处理。合并缓冲区现在通过WithMergeOptions方法来设定。”
 
AsMerged。AsMerged又重命名为曾经使用的和AsParallel类似的名称——AsSequential。

 

二元运算符。涉及两个数据源的LINQ运算符需要两边都进行AsParallel。类似下面这样的运算:

a.AsParallel().AsOrdered().Zip(b, (x, y) => x*y);

  可并行化为:

a.AsParallel().AsOrdered().Zip(b.AsParallel(), (x, y) => x*y);

  或

a.AsParallel().AsOrdered().Zip(b.AsParallel().AsOrdered(), (x, y) => x*y);

  影响到的运算符:Zip、Join、JoinGroupJoin、Concat、SequenceEqual、Union、Intersect、Except。
 
  性能提升

  1.保序流水线处理(Order-preserving pipelining)进行了合并——之前,在查询上一执行AsOrdered,就会在生成单个元素之前迫使整个查询执行。现在进行了优化,以便来自查询的元素只在MergeOptions值为Default(AutoBuffered)和NotBuffered的时候才生成。

  2.针对未实现IList<T>的数据源,提升了分区正确性。

  3.某些基于IList<T>或数组的查询具有更好的性能。

  4.大块分区尺寸进行了调整——基于IList<T>和数组这样的数据源(也即非可索引的数据源)的查询,大块分区是最常见的分区方案(partitioning scheme)。随着越来越多的大块分区被访问,他们的尺寸也就不断增长。这是对如下两种情况的一个平衡:a)对小数据集进行查询,但在查询中要进行昂贵的委托处理,b)对大数据集进行查询,但在查询中不用进行昂贵的委托处理。

  5.消除了有可能存在的错误共享情况,在某些情况下可以提升6倍的性能。

  删除了很少用到的运算符。某些运算符是为了性能原因而创建的,但是并没有为LINQ提供任何性能好处,因而被移除了。哪些运算符要移除并未确定。

原创粉丝点击