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();
下面是它可能的输出之一(因为无序,所以并不确定)
0 4 8 9 1 3 5 6 2 7
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.For
和 Parallel.ForEach
,然并卵,远不如原生的 for
和 foreach
易用。纯粹从语法上来说,是的,但是要了解到 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<T>
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();
}