初识Parallel Extensions之PLINQ

 

初识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>

你可能感兴趣的:(LINQ)