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 會盡量的同時處理這些數——它是異步的,也就意味着,它是無序的。學習

來舉個簡單的例子pwa

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。仍是剛纔那個例子,稍稍改動下code

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 毫秒。it

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();
}
相關文章
相關標籤/搜索