.NET4.0并行计算技术基础(12)——下
 

19.4.3 深入探索PLINQ

         前面的章节已经对PLINQ编程做了介绍,本节我们来探讨一下PLINQ背后的运作机理,以便更好地使用PLINQ

1 “自适应”的运行模式

         如果代码中使用AsParallel()扩展方法将LINQ查询转换为PLINQ,在程序运行时,TPL会自动分析此查询能否并行执行,如果可以,再考虑这种并行执行能否获得性能的提升,否则,仍然采用串行执行方式。这就是说:
    PLINQ查询并不一定以并行方式执行。
         虽然PLINQ的设计者已经对很多种典型的场景进行了分析并且选择了相应的PLINQ查询运行模式,但不可能让PLINQ默认的方案适用于所有的场景,因此,如果你确信在任何情况下你的算法都是可以并行执行的,那么,你可以指定PLINQ不再进行分析而直接采用并行模式。
         请看以下代码:
 
var parallelQuery = (from item in datasource.AsParallel()
    .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
    select item;
 
         上述代码使用ParallelEnumerable.WithExecutionMode()扩展方法强制以并行方式运行PLINQ查询。此方法接收一个ParallelExecutionMode枚举类型的参数用于指定执行模式。

2 设置工作线程数

         默认情况下,到底使用多少个线程来执行PLINQ是在程序运行时由TPL决定的。但是,如果你需要限制执行PLINQ查询的线程数目(通常需要这么做的原因是有多个用户同时使用系统,为了服务器能同时服务尽可能多的用户,必须限制单个用户占用的系统资源),我们可以使用ParallelEnumerable. WithDegreeOfParallelism()扩展方法达到此目的。
         请看以下示例代码:
 
var parallelQuery = from item in datasource.AsParallel()
    .WithDegreeOfParallelism(4)
    select item;
 
         上述代码强制使用4个线程来执行PLINQ查询。
 
       多懂一点:
设定执行并行循环的工作线程数
       PLINQ类似,我们也可以设定执行并行循环的工作线程数。
       在使用Parallel.For(或Parallel.ForEach)启动循环时,可以给其提供一个ParallelOptions类型的参数,并指定其MaxDegreeOfParallelism字段值。
 
    ParallelOptions opt = new ParallelOptions();
    opt.MaxDegreeOfParallelism = 4;
    Parallel.For(0, 100, opt, (i) => Process(i));
 
       上述代码中指定最多用4个线程执行并行循环。
        读者需要注意区分并行循环中使用的ParallelOptions.MaxDegreeOfParallelismPLINQ中出现的WithDegreeOfParallelism()扩展方法。
       ParallelOptions.MaxDegreeOfParallelism指明一个并行循环最多可以使用多少个线程。TPL开始调度执行一个并行循环时,通常使用的是线程池中的线程,刚开始时,如果线程池中的线程很忙,那么,可以为并行循环提供数量少一些的线程(但此数目至少为1,否则并行任务无法执行,必须阻塞等待)。等到线程池中的线程完成了一些工作,则分配给此并行循环的线程数目就可以增加,从而提升整个任务完成的速度,但最多不会超过ParallelOptions.MaxDegreeOfParallelism所指定的数目。
       PLINQWithDegreeOfParallelism()则不一样,它必须明确地指出需要使用多少个线程来完成工作。当PLINQ查询执行时,会马上分配指定数目的线程执行查询。
       之所以PLINQ不允许动态改变线程的数目,是因为许多PLINQ查询是“级联[1]”的,为保证得到正确的结果,必须同步参与的多个线程。如果线程数目不定,则要实现线程同步非常困难。


[1]  所谓“级联”,是指一个复杂的PLINQ查询可能包容多个的子查询,而这些子查询又可以包容它自己的子查询,从而形成一个多层嵌套的查询语句。
 

3 工作线程的数据提取策略

         PLINQ查询经常需要处理大量的数据,而这些处理工作是由线程执行的,为了实现并行处理,需要仔细考虑工作线程的数据存取方式。
         方式一:将所有数据全部装入到一个“临时”数组中,然后,将这个数组分成“几块”,交由不同的线程执行。
         这种方式的优点是简单直观,可以实现高度的并行性,但缺点也是明显的:
         1 有可能需要使用巨大的内存空间。
         2 只有等数据全部装入完毕才能进行处理工作。
         方式二:根据需要提取数据。让所有的工作线程都共享同一个输入源,当某个线程需要访问数据时,它锁定此资源,取出数据,然后再释放锁。这实际上就是我们在第17章所介绍过“互斥”访问共享资源机制。这种方式也有几个缺陷:
         1 可能需要进行频繁的线程上下文切换,性能不好。
         2 无法对数据进行缓存,因为一次只从原始数据源中提取所需要的数据,而不能一次多提取一点“备用”。
         3)需要锁定共享资源,会导致程序性能受损。
         方式三:每次线程提取数据时都采用“批发”方式,比如一次提取64项。这就大大减少了锁定共享资源的需求。这个方法看上去不错,但仍有问题:
         假设要处理的数据量很小(比如数据项数小于最小“批发数量”-64项),但每个数据项要处理较长时间时,这个策略将导致事实上的“顺序”处理数据项,因为一个线程将所有“存货”都提取走了,其它线程都“无货可提”,导致“并行计算”有名无实。
         方式四:先将数据分区,为每个分区分配一个线程。每个线程第一次从所关联的分区中提取1项,第二次提取2项,第三次提取4项,……,第n次提取2(n-1)项。
         这种策略综合了前述几个方式的优点,是当前版本的PLINQ默认采用的数据提取策略。
 

19.2 并行计算的应用实例

         前面的章节已经对.NET4.0所提供的并行扩展进行了详细介绍,读者一定对并行计算有了深刻的印象,并且您可能开始跃跃欲试地尝试在自己的开发实践中应用并行计算技术了。事实上,并行计算是一个大趋势,拥有无限可能的应用前景。在本节中,我们通过一个应用实例介绍如何将并行计算应用于图像处理领域,然后再介绍一下.NET并行计算领域的未来发展。

19.5.1 图像处理——并行计算的应用实例

         在计算机中显示的每张图像都由许多单个的像素构成,计算机图像处理通常可归结为对这些像素数据的算术操作。
         组成彩色图像的每个像素的颜色通常都包含RGB三个分量,通过修改特定像素的颜色,我们就实现了对图像的颜色变换。例如,如果把某张图像的所有像素的颜色值取反,我们就可以得到类似于“底片”的效果。我们可以在计算机图形学领域找到许多图像变换公式,将这些公式施加于像素的颜色值,就可以实现许多图像处理特效(比如锐化、模糊、彩色图转为灰色图等)。许多图像处理算法都是针对单个像素进行的,彼此之间相互独立,因此,在这个领域非常适合于应用“并行计算”来提升程序性能。
         示例程序MyImageProcessor展示了一个简单的图像处理程序,它可以将一幅彩色图像反转为“底片”效果。
示例程序将图像的像素颜色数据装入到一个字节数组ImagePixelData中,然后,使用Parallel.For()方法对所有像素的颜色值取反,再显示到屏幕上,以下是实现并行图像处理的核心代码:
 
Parallel.For(0, ImagePixelData.Length, (i) =>
                {
                    byte value = ImagePixelData[i];
                    ImagePixelData[i] = (byte)(~value);
                });
 
   有关此示例程序的技术关键点请看本节的“多懂一点”。
多懂一点:
示例程序MyImageProcessor的学习指导
       示例程序MyImageProcessor是一个WPF应用程序,下面简要介绍一下它的技术关键点。
       WPF中,抽象类BitmapSource类用于指代一个图像,其子类BitmapImage代表一个“真实”的图像对象。
       把图像文件路径字串作为参数,调用BitmapImage的构造函数可以创建一个BitmapImage对象。
       WPF中,显示图像使用的是Image控件,只需将它的Source属性设置为一个BitmapSource对象,它就能显示指定的图像,参见以下代码
 
    BitmapSource bmpSource = new BitmapImage(new Uri(图像文件名));
    image1.Source=bmpSource;  //显示图像
 
       示例程序的关键之处在于如何从BitmapSource对象中提取像素的颜色数据。这里需要了解一下计算机图像处理领域的基础知识。
       每个图像都有一个以像素个数为单位的尺寸,比如我们常用于设置桌面背景的壁纸通常拥有1024*768的尺寸,这个尺寸指的是“图像宽为1024个像素,高为768像素”。
       对于不同类型的图像文件,很有可能每个像素所关联的数据量是不同的,比如有的图像使用3个字节来保存像素颜色的RGB三个分量,而有的则使用4个字节来保存像素的颜色数据(在RGB三个分量的基础上再加上一Alpha值,用于表示颜色的透明度)。
       幸运的是,通过BitmapImage对象的Format.BitsPerPixel属性我们可以知道每个像素占用的位数,将其除以8就得到了单个像素所占用的字节数,而不需要编写代码处理各种类型的图像文件。
       另一个知识点是需要知道图像每行像素数据的总字节数(这个数值在计算机图像处理领域被称为做位图图像的“stride”值)。为了提升性能,通常要求这个数值能被4整除,但图像文件不可能总满足这个要求,为此,有可能需要“补”上若干个字节以“凑”成一个可被4整除的数。
       WPF中,BitmapSource类提供了一个CopyPixels()方法可以将图像的像素数据复制到一个字节数组里,而另一个它的另一个Create()方法可以从字节数组中重新提取数据创建一个新的BitmapSource对象。
       掌握了以上知识,就能看得懂示例程序中的代码了,以下是示例程序中的代码片断:
 
       BitmapSource bmpSource=null;
        int stride=0;
        byte[] ImagePixelData=null;
        // 装入图像的像素数据到字节数组中。
        private void LoadImage(string ImagePath)
        {
         //创建BitmapSource对象
            bmpSource = new BitmapImage(new Uri(ImagePath));
           //计算图像的stide
            stride = bmpSource.PixelWidth * bmpSource.Format.BitsPerPixel / 8;
            stride +=  4-stride % 4;  //补上几个字节,使其可以被4整除
            //创建字节数组,注意其大小要合适,可以放得下所有的图像数据
            int ImagePixelDataSize = stride * bmpSource.PixelHeight *
                 bmpSource.Format.BitsPerPixel / 8;
            ImagePixelData = new byte[ImagePixelDataSize];
            //复制图像数据到字节数组中
            bmpSource.CopyPixels(ImagePixelData, stride, 0);
        }
 
       其余的代码就很好懂了,请读者自行阅读示例源码。

19.6.2 并行计算的未来之路

         当前计算机中普遍装备了“双核”CPU,一些新购置的计算机更是装备了“四核”CPU,随着CPU在“多核化”之路上越走越远,并行计算已成为软件技术确定无疑的发展方向。
         CPU多核化趋抛同时出现的是计算机网络的“无孔不入”,由此可知,分布式的软件系统也将成为软件技术发展的另一个方向,而分布式的软件系统“天生”就是“并行”的,因此,未来的软件系统一定同时兼具有“并行”和“分布”两大特点
         要开发并行的程序,可以利用.NET 4.0新加入的并行扩展;要开发分布式的软件系统,可以使用.NET 4.0中功能得到进一步增强的WCF。本书第9篇将详细WCF
         如果将本章介绍的并行计算技术与本书第9篇介绍的WCF技术结合起来,在微软平台之上,我们就拥有了前所未有的强有力的开发工具,可以用它来开发高度分布和高度并行的软件系统,解决更为复杂的问题,其应用前景非常广阔。
         我们看到,.NET 4.0已经在并行计算之路上迈出了第一步,而这一步一旦迈出,就不会停止下来。
         并行计算的大幕刚刚拉开,精彩的剧目即将上演,让我们拭目以待吧!
============================
全文完
请看本系列文章结束语——《迎接新一轮的技术进步浪潮