代码改变世界

Effective C# 学习笔记(三十五) 了解PLINQ如何实现并行算法

2011-07-21 22:11  小郝(Kaibo Hao)  阅读(500)  评论(0编辑  收藏  举报

AsParallel() 是PLINQ的一个方法,它使得并行编程变得简单,但是其并不能解决并行编程中的所有问题。

首先举例说明如何对于一个顺序执行的逻辑启用并行运行特性,看如下代码:

var numsParallel = from n in data.AsParallel() //这里用了AsParallel()方法进行并行调用

where n < 150

select Factorial(n);

 

并行的第一步是对要执行的数据进行划分,以保证由多核创建创建出的多线程能分别执行,以提高运行效率。PLINQ使用4中划分算法:

 

  1. Range partitioning(平均分配法)

即将要分配的数据平均分配到多个任务执行。数据/并行任务个数=每个任务执行的数据量

这种算法多用于支持索引,并且可获取数据数量的集合类型。主要是实现了IList<T>接口的集合类型,如List<T>array

 

  1. Chunk partitioning(区块分配法)

这种算法将要执行的数据按固定大小(目前据我所知不可控),分配给每个线程,当某个线程完成部分区块的处理后,按该算法就会把新的区块分配给该闲置线程,直到所有的线程处理完毕。区块的大小也会随处理逻辑花费时间,和要处理数据的多少(由where子句过滤的数据)而变化。

 

  1. Stripe partitioning(条纹分配法)

该算法是Range partitioning 的一种特例,其根据固定步长来跳格处理数据。如有四个任务来处理数据,第一个任务处理04812,第二个任务处理15913,其他类似。其避免了为了内部线程同步去实现TakeWhile()SkipWhile()方法。

 

  1. Hash Partitioninig(哈希分配法)

这种算法主要是为了解决带JoinGroupjoinGroupByDistinctExceptUnionIntersector操作的查询。其保证了生成相同hash的数据项被同一个任务处理。其最小化了线程间内部通信。

 

线程间调度算法

  1. Pipelining

由一个线程来完成遍历工作,每遍历一个数据交给一个线程处理,若该线程处理完毕,其就可以处理新的数据了。而线程创建的个数一般和你CPU的核数相当。也就是说为了完成一个整体任务,一般会创建(CPU核数+1)个线程。

 

  1. Stop & Go

该算法会调用所有线程来处理数据,多出现在Query查询执行ToList()ToArray(),或PLINQ需要处理所有数据时。使用该算法在可用内存较多的情况下可以提供运算性能,如:

var stopAndGoArray = (from n in data.AsParallel()

where n < 150

select Factorial(n)).ToArray();

var stopAndGoList = (from n in data.AsParallel()

where n < 150

select Factorial(n)).ToList();

注意:上面的代码都是先构建Query查询,再执行stop & go算法的,这样可以提高性能。若是先执行Stop & go 算法,再去合并结果集就会造成线程过多,反而影响了性能。

 

  1. Inverted Enumeration.

这种算法对于枚举来说性能一般来说时最高的。该算法比Stop & Go 算法使用的内存空间小,其性能取决于对于查询的结果要执行的操作所花费的时间。是哦那个该算法你应该对数据调用AsParallel()方法,并在调用结果时调用ForAll()方法。如下代码所示:

var nums2 = from n in data.AsParallel() //调用并行运算方法

where n < 150

select Factorial(n);

nums2.ForAll(item => Console.WriteLine(item));//调用ForAll() 方法

 

LINQ是懒加载数据的,也就是说只有你在访问被LINQ描述的查询结果集时它才执行查询的运算。而对于每个数据的运算都是单个运行的。而对PLINQ,来说其更像 Linq to SQL Entity framework,当你获取第一个对象时,它返回的是整个数据集。所以要小心使用,以免造成不必要的内存占用和性能损失。下面的代码说明了LINQ to Objects PLINQ 对于数据的不同处理方式。

static class ParallelTest

    {

     //测试数据长度

        private static int testLen = 30;

     //扩展方法where子句过滤执行方法

        public static bool SomeTest(this int inputValue)

        {

            Console.WriteLine("testing element: {0}", inputValue);

            return inputValue % 10 == 0;

        }

     //扩展方法Select子句投影执行方法

        public static string SomeProjection(this int input)

        {

            Console.WriteLine("projecting an element: {0}", input);

            return string.Format("Delivered {0} at {1}",

            input.ToString(),

            DateTime.Now.ToLongTimeString());

        }

     //测试Linq To Objects 顺序执行

        public static void TestSequence()

        {

            var answers = from n in Enumerable.Range(0, testLen)

                          where n.SomeTest()

                          select n.SomeProjection();

 

            var iter = answers.GetEnumerator();

            Console.WriteLine("About to start iterating");

            while (iter.MoveNext())

            {

                Console.WriteLine("called MoveNext");

                Console.WriteLine(iter.Current);

            }

        }

     //测试PLINQ并行执行

        public static void TestParallel()

        {

            var answers = from n in ParallelEnumerable.Range(0, testLen)

                          where n.SomeTest()

                          orderby n.ToString().Length

                          select n.SomeProjection().Skip(20).Take(20);

 

            var iter = answers.GetEnumerator();

            Console.WriteLine("About to start iterating");

            while (iter.MoveNext())

            {

                Console.WriteLine("called MoveNext");

                Console.WriteLine(iter.Current);

            }

            Console.Read();

        }

 

--------------------LINQ to Objects 运行结果--------------------

About to start iterating

testing element: 0

projecting an element: 0

called MoveNext

Delivered 0 at 9:35:39

testing element: 1

testing element: 2

testing element: 3

testing element: 4

testing element: 5

testing element: 6

testing element: 7

testing element: 8

testing element: 9

testing element: 10

projecting an element: 10

called MoveNext

Delivered 10 at 9:35:40

testing element: 11

testing element: 12

testing element: 13

testing element: 14

testing element: 15

testing element: 16

testing element: 17

testing element: 18

testing element: 19

testing element: 20

projecting an element: 20

called MoveNext

Delivered 20 at 9:35:40

testing element: 21

testing element: 22

testing element: 23

testing element: 24

testing element: 25

testing element: 26

testing element: 27

testing element: 28

testing element: 29

 

从上面的运行结果来看,每一步都是顺序执行的,先过滤数据,然后选择出合适条件的数据

-------------PLINQ运行结果-------------

 

About to start iterating

testing element: 0

projecting an element: 0

testing element: 15

testing element: 16

testing element: 17

testing element: 18

testing element: 19

testing element: 20

testing element: 1

testing element: 2

testing element: 3

testing element: 4

testing element: 5

testing element: 6

projecting an element: 20

testing element: 7

testing element: 8

testing element: 9

testing element: 21

testing element: 22

testing element: 10

projecting an element: 10

testing element: 11

testing element: 12

testing element: 23

testing element: 24

testing element: 25

testing element: 26

testing element: 27

testing element: 28

testing element: 13

testing element: 14

testing element: 29

called MoveNext

Delivered 0 at 9:35:40

called MoveNext

Delivered 20 at 9:35:40

called MoveNext

Delivered 10 at 9:35:40

 

从上面运行结果看,PLINQ的运行逻辑不是顺序的,其先运算并缓存大部分数据然后再在缓存中获取数据,过滤数据和选择输出不是并行的,你不能预测某个数据在何时进行处理。

 

以上实验证明,LINQ to Objects PLINQ对于数据查询的不同处理方式,所以在编写自己的应用逻辑的时候一定要选择合适的方法。