转载

C# 并行计算(Parallel 和 ParallelQuery)

parallel

英 [ˈpærəlel] 美 [ˈpærəˌlɛl]

adj.平行的; 相同的,类似的; [电]并联的; [计]并行的

adv.平行地,并列地

n.平行线(面); 相似物; 类比; 纬线

vt.使平行; 与…媲美; 与…相比; 与…相似

System.Threading.Tasks.Parallel

Parallel 主要提供了 For 系列方法和 ForEach 系列方法,并行处理所有项。两个系列的方法都有若干重载,但最常用也最易用的是这两个

Parallel.For(int fromInclude, int toExclude, Action<int> body)

这个方法从 fromInclude 开始,到 toExclude 结束,循环其中的每一个数,并对其执行 body 中的代码。从参数的名称可以了解,这些数,包含了 fromInclude ,但不包含 toExclude

这很像 for(int i = fromInclude; i < toExclude; i++) { body(i) } ,但与之不同的是 for 语句会顺序的遍历每一个数,而 Parallel.For 会尽可能的同时处理这些数——它是异步的,也就意味着,它是无序的。

来举个简单的例子

Parallel.For(0, 10, (i) => {     Console.Write($"{i} "); }); Console.WriteLine();

下面是它可能的输出之一(因为无序,所以并不确定)

Parallel.ForEach<T>(IEnumerable<T>, Action<T>)

Parallel.For 就是异步的 for ,那么 Parallel.ForEach 就是异步的 foreach 。还是刚才那个例子,稍稍改动下

var all = new [] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Parallel.ForEach(all, (i) => {     Console.Write($"{i} "); }); Console.WriteLine();

其结果同样是 0~9 的无序排列输出。

并行的意义在于处理耗时计算

上面仅仅说明了 Parallel.ForParallel.ForEach ,然并卵,远不如原生的 forforeach 易用。纯粹从语法上来说,是的,但是要了解到 Parallel 的目的是并行,并行的目的是高效处理耗时计算。所以,现在需要假设一个耗计算存在……

void LongCalc(int n) {     // 也没什么用,只是模拟耗时而已     Thread.Sleep(n * 1000); }

如果用 for 循环,不用运行也能知道下面这代码运行时间会超过 6000 毫秒。

for (int i = 1; i < 4; i++) {     LongCalc(i); }

但是用 Parallel.For

Stopwatch watch = new Stopwatch(); watch.Start(); Parallel.For(1, 4, LongCalc); watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds);

答案是 3019 ,意料之中,并行嘛!

如果需要每次 body 调用的计算结果怎么办

这个问题又站在了一个新的高度,毕竟不所有事情都不需要反馈。

现在用一个递归调用的阶乘算法来模拟耗时计算,虽然它其实并不怎么耗时,为了使代码简单,这里并不会选用太大的数。然后假设需要算 n 个数的阶乘之和,所以我们需要并行计算每个数的阶乘,再把各个结果加起来。这段代码大概会是这样

int CalcFactorial(int n) {  return n <= 2 ? n : n * CalcFactorial(n - 1); } int SumFactorial(params int[] data) {  int sum = 0;  Parallel.ForEach(data, n => {   sum += CalcFactorial(n);  });  return sum; } 

给几个数就可以得到结果:

Console.WriteLine($"sum is {SumFactorial(4, 5, 7, 9)}");

猜猜结果是多少?

368064 么?是的。

368064 ,但并不每次都是,有时候可能会得到一个 120 ,或者 5040 ,为什么???

注意 “并行” ,这意味着存在线程安全的问题。

+= 并不是原子操作,所以这里需要改一下

int SumFactorial(params int[] data) {     int sum = 0;     Parallel.ForEach(data, n => {         Interlocked.Add(ref sum, CalcFactorial(n));     });     return sum; }

如你如愿,这回对了。

Interlocked 类提供了一些简单计算的原子操作,完全值得去学习一下。

话虽如此,但需要的不是一个计算好的结果,而是每一个单独的结果怎么办?看起来 Parallel 有点不合适了,那就试试 ParallelQuery 吧,这个来自 Linq 的东东。

System.Linq.ParallelQuery

IEnumerable<T>.AsParallel() 可以很容易得到 ParallelQuery<T> ,这也是 Linq 中提供的扩展方法。那么从熟悉的开始,改用 ParallelQuery<T> 来算算阶乘之和

int SumFactorial(params int[] data) {     return data.AsParallel().Select(CalcFactorial).Sum(); }

很简单的样子。而且 Select() 也很熟悉,它得到的是一个 ParallelQuery<T> ,继承自 IEnumerable<T> 。所以,如果需要每一个单独的结果,只要去掉 Sum() ,换成 ToList() 或者 ToArray() 就可以了,甚至直接作为一个 IEnummerable<T> 来使用也是不错的选择。

这里似乎接触到了一个新的话题——并行 Linq。其实 Linq 也是编译成方法调用来运行的,现在已经有方法调用的代码了,写个 Linq 语句还不容易:

int SumFactorial(params int[] data) {     return (from n in data.AsParallel()             select CalcFactorial(n)).Sum(); }
正文到此结束
Loading...