Parallel.for性能问题

parallel.for一定是用来处理计算限制的操作的,其中涉及到线程同步和委托调用等的性能损耗。所以,对于一个数组中的元素进行简单操作却使用parallel必然会导致性能下降。
如:
public static void MultiplicateArrayParallel(double[] array,double factor){
Parallel.For(0,array.Length,i=>
{
array[i] = array[i]*factor;
});
}

public static void MultiplicateArraySingle(double[] array,double factor){
for(int i =0;i<array.Length;i++)
{
array[i] = array[i]*factor;
}
}

PLINQ以及TPL中的自定义分区
为了并发处理数据集合中的元素,一个有效的措施是对数据集合并行分组,然后并发处理多个分组。PLINQ和TPL(Task Parallel Library)
在内部也会对数据源进行分组,将数据源均分成cpu数量的分数,进行并发处理,当某个线程提前处理完后,会协助别的线程进行处理,
知道所有数据都处理完成,这其中不可避免会涉及线程同步问题而造成性能问题,但是默认分组并非总是最优的,为了更优越的性能,
我们可以自己对数据源进行分组。
一般来讲最有效的数据分组方式是利用多线程协作顺序处理原集合,而不是把原集合分成几个子集合再单独处理。
对于已知长度的集合(如定长数组)最优的方法是范围分区
比如:一个数据有100
个元素,对于一个拥有四个处理器的机器来说,最好的办法是开辟四个线程,第一个线程处理0~24的元素,第二个线程处理25~49的元素,
第三个线程处理50~74的元素,第四个线程处理75~99的元素,而不是将100个元素的数据先分成4个25个元素的子数组,然后再单独处理
这四个字数据。
这样分组的一个不好的地方就是,如果一个线程提前完成了任务,它不会去帮助没完成任务的线程去完成任务。
对于变长集合,如(可变数组)最优的方法是块分区。
块分区中,每个线程或任务会处理一个块中的部分数据,处理完成后,再去获取新的待处理元素。
分区器会保证所有的元素都被处理,不会遗漏也不会重复。每个块的大小也不确定。
一般来讲,范围分区只有在委托执行时间不太长且集合元素很多,且每个分区的工作量差不多时才会加快集合的处理速度。
块分区在大多数情况下都会提升性能,对于元素数量不太多,或者委托执行时间很长的数据源,块分区和范围分区的性能事是差不多的。

TPL分区器也支持动态分区,可以在运行时产生新的分区。此特性允许分区器随着foreach循环的增减而增减。动态分区器内部是负载均衡的。
所以当你创建一个自定义的分区器时,要让动态分区器能被ForEach循环使用

为PLINQ配置负载均衡的分区器
Partitioner.Create 方法支持为一个array或IList数据源创建一个分区器并指定是否支持内部线程的负载均衡。当分区器被配置为
支持负载均衡时将会应用块分区,当请求发生时,元素会被以小块的形式传递给每个分区。此方法保证每个分区都有元素处理,知道所有
数据被处理完成之前。
另外一个重载形势能为所有的IEnumerable数据源提供负载均衡分区器。

通常,负载均衡需要分区不断的向分区器请求元素。相比之下,实现静态分区的分区器能一次性完成分区器的元素分配工作,这并不使用
范围分区和块分区。这比负载均衡需更节省开销,但是会因为线程间的工作量不同造成有的线程早就完成了工作但是有的却花费很长的时间完成工作
而花费更多的执行时间。默认情况下,当传递IList或array,PLINQ总是使用范围分区而不考虑负载均衡。为了使用负载均衡,可以使用
Partitioner.Create方法

// Static partitioning requires indexable source. Load balancing
// can use any IEnumerable.
var nums = Enumerable.Range(0, 100000000).ToArray();

// Create a load-balancing partitioner. Or specify false for static partitioning.
Partitioner customPartitioner = Partitioner.Create(nums, true);

// The partitioner is the query’s data source.
var q = from x in customPartitioner.AsParallel()
select x * Math.PI;

q.ForAll((x) =>
{
ProcessData(x);
});

决定在某个场景中是否使用负载均衡的最好方法是去测验,记录在特定负载和电脑配置下所花费的时间。例如,静态分区在核数不多的多核电脑上
性能较好,但是在核数很多的电脑上性能就很差。
下表列出了Create方法的多种重载形势。这些分区器并不限于在PLINQ和Task中使用,它可以用于任何自定义的并行结构中。
Overload Uses load balancing
Create(IEnumerable) Always
Create(TSource[], Boolean) When the Boolean argument is specified as true
Create(IList, Boolean) When the Boolean argument is specified as true
Create(Int32, Int32) Never
Create(Int32, Int32, Int32) Never
Create(Int64, Int64) Never
Create(Int64, Int64, Int64) Never

为Parallel.ForEach配置静态分区器
在For循环中,循环体是作为一个委托提供的。调用委托的性能损耗根调用一个虚方法差不多。在有些情况下,执行委托所花费的时间非常小
以至于在对比下显得调用委托所消耗的性能非常大。在这种情况下,你可以使用Create的某个重载在数据源上创建一个范围分区器。然后,
将这些范围集合传递给方法体由普通for循环构成的ForEach方法,此方法的好处是,对于每个分区委托只调用一次,而不像之前没个元素
都要调用一次委托。下面的示例演示了此方法的基础版本。
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main()
{

    // Source must be array or IList.
    var source = Enumerable.Range(0, 100000).ToArray();

    // Partition the entire source array.
    var rangePartitioner = Partitioner.Create(0, source.Length);

    double[] results = new double[source.Length];

    // Loop over the partitions in parallel.
    Parallel.ForEach(rangePartitioner, (range, loopState) =>
    {
        // Loop over each range element without a delegate invocation.
        for (int i = range.Item1; i < range.Item2; i++)
        {
            results[i] = source[i] * Math.PI;
        }
    });

    Console.WriteLine("Operation complete. Print results? y/n");
    char input = Console.ReadKey().KeyChar;
    if (input == 'y' || input == 'Y')
    {
        foreach(double d in results)
        {
            Console.Write("{0} ", d);
        }           
    }
}

}
循环中的每个线程接受各自的元组Tuple<T1,T2>,这些元组包含了指定子范围的开始和结束索引。内部的for循环直接使用它们在array
或IList中执行循环。

Create方法的一种重载允许你指定分区器的大小和分区器的个数。此重载方法的适用场景是:每个元素的工作非常慢,以至于甚至为每个元素执行一个
虚方法的调用都会对性能有显著的影响。

自定义分区器
一些场景中,需要自定义分区器。如,对于自定义的集合,基于对集合内部的了解,自定义分区器可以拥有比默认分区器更好的性能。
创建一个自定义的分区器需要一个继承于System.Collections.Concurrent.Partitioner的类并重载下面列出的一些虚方法
GetPartitions This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator over a distinct partition.
SupportsDynamicPartitions Return true if you implement GetDynamicPartitions, otherwise, false.
GetDynamicPartitions If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.
If the results must be sortable or you require indexed access into the elements, then derive from System.Collections.Concurrent.OrderablePartitioner and override its virtual methods as described in the following table.

GetPartitions This method is called once by the main thread and returns an IList(IEnumerator(TSource)). Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator over a distinct partition.
SupportsDynamicPartitions Return true if you implement GetDynamicPartitions; otherwise, false.
GetDynamicPartitions Typically, this just calls GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

下表提供了额外的信息说明了三种负载均衡分区器是如何实现OrderablePartitioner类的

静态分区器
如果你想要分区器使用于ForEach方法中,必须返回一个不定数量的分区器。这意味着分区器在循环执行的任何时候都能为一个新的分区提供一个新的枚举器
。当循环加入一个新的并发任务时,它为该任何请求一个新的分区。如果需要数据是有序的,继承
System.Collections.Concurrent.OrderablePartitioner,为每个分区中的每个元素分配一个唯一编号。

分区器协定
1.当你实现一个自定义的分区器的时候,循环以下规范有助于保证于PLINQ和TPL中的ForEach的互操作性
如果GetPartitions调用时传递了0或者比partitionsCount小的数,抛出ArgumentOutOfRangeException。尽管PLINQ和TPL
永远不会传递一个为0的partitionCount,但我们建议您防范这种可能性。
2.GetPartitions和GetOrderablePartitions应该始终返回partitionsCount数目的分区器。如果分区器超出了数据并且不能
创建和请求一样多的分区器,方法应该为每个遗留的分区返回一个空的枚举器。否则,PLINQ和TPL将会抛出
InvalidOperationException
3.GetPartitions,GetOrderablePartitions,GetDynamicPartitions以及GetOrderableDynamicPartitions永远不应该返回
null。如果返回了null,PLINQ和TLPL应该抛出InvalidOperationException.
4.返回分区的方法应该总是返回完整和唯一枚举数据源的分区。除非特殊设计,否则分区包含的数据不能多也不能少。如果此规则
不被遵守,结果的输出顺序将会被打乱。
5.为了保证结果的输出顺序,下列布尔类型的读方法应该总是精确返回以下值
*KeyOrderedInEachPartition:每个分区返回递增健索引的元素
*KeyOrderedAcrossPartitions:对于返回的所有分区,分区i中的键索引比i-1分区中的键索引大
*KeyNormalized:所有键索引都从0单调递增,没有间隙。
6.所有的索引必须唯一。不能有重复的索引
7.所有的索引必须非负。如果此规则不被遵守,PLINQ和TPL会抛出异常。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值