一、并行循环基础1、.NET中含并行for循环也含并行foreach循环,且在并行Linq(PLing)查询语言也是有效。
顺序循环执行的过程,是有序的;而并行循环执行的顺序是不可预测的,一些高值的索引也许会在低值索引之前被执行。
顺序循环可能我们习惯于这样写:
// Sequential for loop
public double[] Chapter2Example01()
{
double[] result = new double[NumberOfSteps];
for (int i = 0; i < NumberOfSteps; i++)
{
result[i] = DoWork(i);
}
return result;
}
// LINQ 1 (sequential)
public double[] Chapter2Example01b()
{
return Enumerable.Range(0, NumberOfSteps)
.Select((i) => DoWork(i)).ToArray();
}
// LINQ 2 (sequential)
public double[] Chapter2Example01c()
{
return (from i in Enumerable.Range(0, NumberOfSteps)
select DoWork(i)).ToArray();
}
而对应的并行循环可能会如下编写:
public double[] Chapter2Example02()
{
double[] result = new double[NumberOfSteps];
Parallel.For(0, NumberOfSteps, (i) =>
{
result[i] = DoWork(i);
});
return result;
}
public double[] Chapter2Example03() //PLinq 1
{
double[] result = new double[NumberOfSteps];
ParallelEnumerable.Range(0, NumberOfSteps)
.ForAll((i) => { result[i] = DoWork(i); });
return result;
}
public double[] Chapter2Example04a() //PLinq 2
{
return (from i in ParallelEnumerable.Range(0, NumberOfSteps).AsOrdered()
select DoWork(i)).ToArray();
}
public double[] Chapter2Example04b()
{
return ParallelEnumerable.Range(0, NumberOfSteps)
.AsOrdered()
.Select((i) => DoWork(i)).ToArray();
}
// optimized for small units of work, each of the same duration
// avoids false sharing
// not appropriate if iteration steps of unequal duration
public double[] Chapter2Example06()
{
double[] result = new double[NumberOfSteps];
Parallel.ForEach(Partitioner.Create(0, NumberOfSteps),//创建分区区间
(range) =>//并行执行每个分区区间中元素
{
for (int i = range.Item1; i < range.Item2; i++)
{
result[i] = DoWork(i);
}
});
return result;
}
public double[] Chapter2Example07()
{
double[] result = new double[NumberOfSteps];
int rangeSize = NumberOfSteps / (Environment.ProcessorCount * 10);
Parallel.ForEach(Partitioner.Create(0, NumberOfSteps, rangeSize >= 1 ? rangeSize : 1),
(range) =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
result[i] = DoWork(i);
}
});
return result;
}
2、控制并行度
“并行度”,.NET也用这个术语来指并行循环中同时执行的任务数量。
在大多数情况下,Parallel 类实现、默认任务调度程序和.NET线程池都能够优化吞吐量。降低并行度往往用于性能测试,以模拟性能较差的硬件。
可以设置ParallelOptions 对象的 MaxDegreeOfParallelism 属性达到控制并行循环同时执行的最大任务数的目的。
public double[] Chapter2Example40()
{
double[] result = new double[NumberOfSteps];
ParallelOptions parallelOptions = new ParallelOptions { MaxDegreeOfParallelism= 2};
Parallel.For(0, NumberOfSteps,
parallelOptions,
() => { return new Random(); },
(i, loopState, random) =>
{
result[i] = random.NextDouble();
return random;
},
_ => { });
return result;
}
3、在循环体中使用局部任务状态
通过使用线程本地数据,您可以避免将大量的访问同步为共享状态的开销。 在任务的所有迭代完成之前,您将计算和存储值,而不是写入每个迭代上的共享资源。 然后,您可以将最终结果一次性写入共享资源,或将其传递到另一个方法。
static void Main()
{
int[] nums = Enumerable.Range(0, 1000000).ToArray();
long total = 0;
// Use type parameter to make subtotal a long, not an int
Parallel.For<long>(0, nums.Length,
() => 0,
(j, loop, subtotal) =>
{
subtotal += nums[j];
return subtotal;
},
(x) => System.Threading.Interlocked.Add(ref total, x)
);
Console.WriteLine("The total is {0}", total);
Console.WriteLine("Press any key to exit");
Console.ReadKey();
}
此次调用的方法原型:
//
// 摘要:
// 执行 for loop 操作,其中可能会并行运行迭代。
//
// 参数:
// fromInclusive:
// 开始索引(含)。
//
// toExclusive:
// 结束索引(不含)。
//
// localInit:
// 用于返回每个线程的本地数据的初始状态的函数委托。
//
// body:
// 将为每个迭代调用一次的委托。
//
// localFinally:
// 用于对每个线程的本地状态执行一个最终操作的委托。
//
// 类型参数:
// TLocal:
// 线程本地数据的类型。
//
// 返回结果:
// 一个 System.Threading.Tasks.ParallelLoopResult 结构,其中包含有关已完成的循环部分的信息。
//
// 异常:
// System.ArgumentNullException:
// 当 body 参数为 null 时引发的异常。- 或 -当 localInit 参数为 null 时引发的异常。- 或 -当 localFinally
// 参数为 null 时引发的异常。
//
// System.AggregateException:
// 为包含从指定委托引发的异常而引发的异常。
public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
每个 For 方法的前两个参数都指定起始迭代值和结束迭代值。在方法的此重载中,第三个参数是在其中初始化本地状态的参数。"此上下文中的“本地状态”是指其生存期恰好从当前线程上循环的第一个迭代之前延伸至最后一个迭代之后的变量。
第三个参数的类型为 Func<TResult>,其中 TResult 是将存储线程本地状态的变量的类型。请注意,在此示例中使用了方法的泛型版本,并且类型参数为 long(在 Visual Basic 中为 Long)。类型参数告知编译器将要用于存储线程本地状态的临时变量的类型。此示例中的 () => 0(在 Visual Basic 中为 Function() 0)表达式表示线程本地变量的初始值为零。如果类型参数是引用类型或用户定义的值类型,则此 Func 将如下所示:
() => new MyClass()
第四个类型参数是在其中定义循环逻辑的参数。IntelliSense 显示其类型为 Func<int, ParallelLoopState, long, long> 或 Func(Of Integer, ParallelLoopState, Long, Long)。lambda 表达式按对应于这些类型的相同顺序需要三个输入参数。最后一个类型参数是返回类型。在这种情况下,类型为 long,因为该类型是我们在 For 类型参数中指定的类型。我们在 lambda 表达式中调用该变量 subtotal,并将其返回。返回值用于在每个后续的迭代上初始化小计。您也可以将此最后一个参数简单地看作传递到每个迭代,然后在最后一个迭代完成时传递到 localFinally 委托的值。第五个参数是在其中定义方法的参数,当此线程上的所有迭代均已完成后,将调用该方法一次。输入参数的类型同样也对应于 For 方法的类型参数,以及主体 lambda 表达式返回的类型。在此示例中,将采用线程安全的方式在类范围将值添加到变量。通过使用线程本地变量,我们避免了在每个线程的每个迭代上写入此类变量。
再比如例子:
// using task-local state for iteration, with partitioner
public double[] Chapter2Example41()
{
double[] result = new double[NumberOfSteps];
Parallel.ForEach(Partitioner.Create(0, NumberOfSteps), //ForEach迭代是分区对象
new ParallelOptions(),
() => { return new Random(); },
(range, loopState, random) =>
{
for (int i = range.Item1; i < range.Item2; i++)
result[i] = random.NextDouble();
return random;//用做下次随机Seed
},
_ => { });
return result;
}
参考资源:
1、<设计模式——.NET并行编程> author:Colin Campbell\Ralph Johnson\Ade Miller\Stephen Toub 清华大学出版社
2、Microsoft MSDN http://msdn.microsoft.com/zh-cn/library/vstudio/dd460703(v=vs.100).aspx