6

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();

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

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.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<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();
}

边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!