.NET4.0并行计算技术基础(5)
 
金旭亮
 
这是一个系列讲座,前面几讲的链接为:
 
.NET 4.0 并行计算技术基础(1)
.NET 4.0 并行计算技术基础(2)
. NET 4.0并行计算技术基础(3)
NET4.0并行计算技术基础(4)
 
====================================
 

3 使用任务并行库实现并行处理

         上面介绍了基于线程编码实现并行处理的技术要点,可以看到还是比较繁琐的。但通过使用.NET 4.0的并行库,可以简化开发工作。我们略微详细一点地介绍一下示例程序中是如何使用任务并行库实现并行计算的。
         其中的一个关键函数是ForRange()函数,先来看看它的声明:
 
       public static ParallelLoopResult ForRange(
        int fromInclusive, int toExclusive,
        Action body)
        {   //…代码略      }
 
         前两个参数代表要计算的数据在数组中的起始和结束索引,第3个参数是一个Action委托,它引用一个将被并行执行的处理函数。
         在并行计算程序中,任务的分解方式是一个需要仔细考虑的问题,有一种常用的方案就是依据本机所包容的处理器个数来决定并行处理的任务数,可以直接调用.NET基类库中的类来获取这一信息。
 
     int numberOfPartitions = System.Environment.ProcessorCount;
        
         确定了要分解的任务数,就可以算出每个子任务负责处理的数据项数:
 
         // 获取要计算的数据范围
            int range = toExclusive - fromInclusive;
            //计算出每个并行任务要计算的数据个数
            int stride = range / numberOfPartitions;
            if (range == 0) numberOfPartitions = 0;
 
         现在到了关键的部分,我们不是使用线程来执行每个子任务,而是直接调用.NET 4.0任务并行库中的Parallel类来完成这一个工作:
 
            return Parallel.For(0, numberOfPartitions, i =>
            {
             int start = i * stride;
             int end = (i == numberOfPartitions - 1) ? toExclusive : start + stride;
             body(start, end);
            });
        }
 
         Parallel.For()是一个静态方法,它的第3个参数是类型为Action的委托,在这里,我们直接使用Lambda表达式来将一个函数直接“内联”作为For()方法的参数。
         For()方法有一个ParallelLoopResult类型的返回值,可以通过此返回值的IsCompleted属性了解For()方法启动的所有任务是否运行结束。
 
      交叉链接:
       如果读者不熟悉ActionFunc等系统预定义委托的含义及用法,请参看本书第12章《微软的创造——委托揭秘》。
       Lambda表达式则是.NET 3.0引入的特性,与LINQ技术关联紧密,相关内容请参看本书第24章《统一的数据访问模式:LINQ》。
 
         在示例程序中,通过以下代码调用上面定义好的ForRange()方法启动并行计算:
 
//……代码略
double mean = CalcuateMeanInSequence(Data); //计算平均值
 
//启动并行计算
ForRange(0, Data.Length, (start, end) =>
            {
                double sum = 0;
                double temp = 0;
                for (int i = start; i < end; i++)
                {
                    temp = Data[i] - mean;
                    sum += temp * temp;
                }
                //保存结果
                lock (SquareSumLockObject)
                {
                    SquareSumUsedByThread += sum;
                };
            });
//……代码略
 
         注意对ForRange()方法调用的第3个参数又是一个Lambda表达式。建议读者一定要花费一点时间去习惯阅读和编写Lambda表达式,今后您一定会在许多场合看到类似的语法现象。

4 小结

         在本小节中,我们通过一个示例程序对比了“顺序算法”,“使用线程的并行算法”和“基于TPL的并行算法”三种编程方式。
         很明显,顺序算法最简单,最易于理解,直接使用线程实现并行计算的,需要编写的代码最多,调试起来也麻烦,而使用TPL,可以不需要直接地与线程打交道,除了语法上略微“奇怪”和“别扭”一些(呵呵,看多了就习惯了)之外,还是比较简洁的。
         从执行时间上看,顺序算法最快,TPL次之,而使用线程的最慢。
         必须指出,程序运行的快慢与许多因素有关,比如计算机硬件配置,是否采用了优化算法,以及操作系统平台等,上述性能比较结果是在一台双核笔记本+Windows 7下得到的,读者的结果有可能不一样。
         从性能比较的结果来看,并行计算并非总具有性能优势,这也提醒我们要注意并行计算的应用场合:
         1)每个数据项要执行的处理工作量很大,需要耗费较多的时间
         2)要处理的数据集合很大。
         很明显,对于求方差这样一个任务而言,对于每个数据项要执行的操作实在太简单了,只不过是加加减减和乘乘除除,因此,串行算法的性能更优。
         另外,尽管TPL极大地简化了并行程序的开发门槛,在很多情况下不需要直接地与线程打交道,但仍然需要开发者掌握多线程开发的基础知识与基本技能: 比如示例使用lock来互斥地访问多线程共享的变量SquareSumUsedByThread
 
         所以, 多线程技术是开发并行计算程序的基础
 
===============================================
 
下一讲,《.NET4.0并行计算技术基础(6)》介绍使用TPL中的Parallel类实现并行计算。
 
未完待续