这是本系列文章中的最后一篇,与前11讲一起,构成了一个对“.NET 4.0并行计算”技术领域的完整介绍。
微软10月22日刚向公众提供了Visual Studio 2010与.NET 4.0 BETA2的下载链接,而我正在下载当中。BETA2已与正式版非常接近了,在安装完VS2010 BETA2后,所有新旧实例均会转移到此新版本中,我再写的新文章也会针对BETA2。
相信大家都会非常关注VS2010与.NET 4.0,我过几天会发布一篇《迎接新一轮的技术浪潮》作为本系列文章的结束语,谈谈我对.NET 4.0新技术的观点,并介绍我的新著的相关情况。
金旭亮
2009.10.22
附注:由于51CTO对博客的字数限制,所以这一部分不得不分为三部分发出。
============================================
.NET4.0并行计算技术基础(12)——上
这是一个系列讲座,前面几讲的链接为:
================================================
3自定义的聚合函数<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
所谓“聚合函数(Aggregate Function)”,其实就是对数据集合进行某种处理后得到的单一结果,比如统计一批数值型数据的平均值、最大值、最小值等。在PLINQ中,我们可以使用ParallelEnumerable类的扩展方法Aggregate()自定义一个聚合函数。
ParallelEnumerable. Aggregate()有好几个重载形式,我们来看一个最复杂的:
public static TResult Aggregate<TSource, TAccumulate, TResult>(
this ParallelQuery<TSource> source, //指明此扩展方法适用的数据类型
TAccumulate seed, //聚合变量的初始值
//用于更新聚合变量的函数,此函数将对每个数据分区中的每个数据项调用一次
Func<TAccumulate, TSource, TAccumulate> updateAccumulatorFunc,
//用于更新聚合变量的函数,此函数将对每个数据分区调用一次
Func<TAccumulate, TAccumulate, TAccumulate> combineAccumulatorsFunc,
//用于获取最终结果的函数,在所有工作任务完成时调用
Func<TAccumulate, TResult> resultSelector
);
这个函数声明拥有5个参数,看上去有些吓人,但只要耐下心来分析一下,还是可以理解的。
首先,第一个参数的this关键字表明可以对任何一个ParallelQuery<TSource>类型的变量调用Aggregate()方法,请注意ParallelEnumerable. AsParallel< TSource >()方法的声明:
ParallelQuery<TSource> AsParallel<TSource>(
this IEnumerable<TSource> source);
这意味着任何一个实现了IEnumerable<TSource>接口的对象都可以很方便地转换为ParallelQuery<TSource>类型的对象。所以,我们可以使用以下公式来调用自定义聚合函数:
实现了IEnumerable<TSource>接口的对象.AsParall<TSource>().Aggregate< U,T,V>(…);
另外,请牢记所有聚合函数返回单一值,因此,会有一个值在Aggregate()函数的剩余几个参数间“传递”,这个值不妨称之为“聚合变量”。聚合变量的类型由Aggregate()函数的类型参数
TAccumulate
指定。
Aggregate()函数的第2个参数Seed给聚合变量指定一个初始值。
Aggregate()函数的后面几个参数都是处理并修改聚合变量的。这里有一个背景知识:您必须知道PLINQ是如何执行查询的。
在19.3.3小节介绍Parallel.For和Parallel.ForEach时,曾介绍过数据“分区”的概念。不妨重述如下:
当有一批数据需要处理时,TPL会将这些数据按照内置的分区算法(或者你可以自定义一个分区算法)将数据划分为多个不相交的子集,然后,从线程池中选择线程并行地处理这些数据子集,每个线程只负责处理一个数据子集。
回到针对“自定义聚合函数”的讨论中来,在这里,TPL会将指定的数据处理函数应用于每个数据子集中的每个元素,然后,再把每个数据子集的处理结果(由“聚合变量”所保存)组合为最终的处理结果。
现在我们可以讨论Aggregate()函数的剩余几个参数的含义了。
Aggregate()函数的第3个参数updateAccumulatorFunc用于引用一个数据处理函数,针对每个数据分区中的每个数据项,此函数都会调用一次。请注意这个被多次调用的函数接收两个参数,一个是聚合变量,另一个则是数据分区中的每个数据项,函数返回值将作为聚合变量的“新值”。另外,要注意对于每个数据分区都关联着一个不同的聚合变量,而对于每个数据分区而言,是以“串行”方式对每个数据项调用数据处理函数的,因此,在数据处理函数内部不需要给聚合变量加锁就可以安全地访问它。
当所有数据分区的数据处理工作完成以后,每个数据分区会产生一个结果,此结果由本分区关联的“聚合变量”保存,由此得到了另一个数据集合:
{ 分区1的处理结果,分区2的处理结果,……,分区n的处理结果 }
Aggregate()函数的第4个参数combineAccumulatorsFunc引用另一个数据处理函数对此“数据集合”进行处理。此数据处理函数的第一个参数也是“聚合变量”,第二个参数代表上述数据集合中的每个数据项,此数据处理函数的返回值将成为“聚合变量”的新值。
现在开始介绍Aggregate()函数的最后一个参数resultSelector,同样地,此参数也引用一个数据处理函数,这个函数只有一个参数,其值就是前面两个数据处理函数被执行之后所得到的“聚合变量”的最终值。resultSelector引用的函数可以对这个“聚合变量”进行最后的“加工”,得到整个Aggregate()函数的最终处理结果。
相信上述文字可能会让读者“头大”了,通过一个实例可能更好理解。我们在第19.3.2节中介绍过使用TPL计算数据的总体方差,为方便起见,这里将求方差的公式重新列出:
请看示例项目
UseAggregateFunc
,它使用聚合函数来计算方差,为简化起见,数据集合为
5
个随机生成的
1~10
间的整数。某次运行结果如下:
分析图
19‑22
,我们可以发现:
TPL
将数据分为两个“区”,一个区包含
2
个数据,由线程
9
负责处理,另一个区包含
3
个数据,由线程
6
负责处理。
请注意每个线程刚开始执行时,聚合变量
aggValue
值都为初始值
0
,每次执行数据处理函数
updateAccumulatorFunc
时,其返回值都成为
aggValue
的新值。
等每个分区数据处理完成时,得到一个新的“数据集合”,其成员为两个分区的“聚合变量”的当前值:
{14,4}
这时另一个数据处理函数
combineAccumulatorsFunc
被调用,将两个分区的处理结果累加起来。在示例中,只有两个数据分区,所以只需调用一次数据处理函数即可。如果有多个分区结果需要组合,此数据处理函数可能会调用多次。
以下列出这个示例程序中的聚合函数代码片断,请读者仔细阅读注释:
//生成测试数据放到整型数组source中...(代码略)
//计算平均值
double mean = source.AsParallel().Average();
Console.WriteLine("总体数据平均值={0}", mean);
//并行执行的聚合函数
double VariantOfPopulation = source.AsParallel().Aggregate(
0.0, //聚合变量初始值
//针对每个分区的每个数据项调用此函数
(aggValue, item) => {
double result = aggValue + Math.Pow((item - mean), 2);
Console.WriteLine(……);
return result;
},
//针对分区处理结果调用此函数
(aggValue, thisDataPartition) =>
{
double result = aggValue + thisDataPartition;
Console.WriteLine(……);
return result;
},
//得到最终结果
(result) => result / source.Length
);
//输出最终结果
Console.WriteLine("数据的方差为:{0}", VariantOfPopulation);
使用聚合函数比较繁琐,不易理解,但代码量较小,程序执行性能也不错,更关键的是它不需要考虑线程同步问题,还是比直接使用线程和
Task
更方便,因此,还是值得花费点时间弄明白它的用法。
4中途取消PLINQ操作
PLINQ
采用统一线程取消模型来中途取消查询操作,只需在查询中使用
WithCancellation()
扩展方法就行了。
以下是示例代码:
CancellationTokenSource cs = new CancellationTokenSource();
//……
var query=from data in DataSource.AsParallel().WithCancellation(cs.Token)
select data;
//……
当
CancellationToken
置于
signaled
状态时
,
PLINQ
会抛出一个
OperationCanceledException
异常
,
只需捕获此异常即可响应外界发出的
“取消”请求。
示例
PLINQCancel
展示了如何中途取消
PLINQ
操作,请读者自行阅读源代码。
提示:
由于PLINQ在底层使用TPL,所以,PLINQ的异常处理机制与TPL的一致。
======================================
转载于:https://blog.51cto.com/bitfan/215152