CodeGym /課程 /C# SELF /資料的並行處理

資料的並行處理

C# SELF
等級 60, 課堂 3
開放

1. 介紹

今天我們會看看如何用你電腦(或伺服器)上可用的所有處理器核心,盡可能快地處理大量資料。為此我們會用到 System.Threading.Tasks.Parallel 裡的類別,主要是 Parallel.For 與 Parallel.ForEach 方法。

如果任務是純 CPU-bound,要怎麼做?

傳統的 for 或 foreach 會一個接一個地處理元素,簡單可靠。但在多核心處理器上,這樣的迴圈只會用到一個核心,其他核心閒置。何不把陣列的不同片段分配給不同核心,讓它們同時處理呢?

範例:


// 計算從 1 到 N 的平方和
long sum = 0;
for (int i = 1; i <= 1_000_000; i++)
{
    sum += i * i;
}

這段程式很簡單,但是順序執行的。如果把工作分散到多個核心會怎樣?

認識家族成員:Parallel.For 與 Parallel.ForEach

這是什麼?

  • Parallel.For — 類似一般的 for,但會把工作切成多個部份並自動分配到不同的 thread,利用所有可用的核心。
  • Parallel.ForEach — 跟一般的 foreach 一樣處理集合,但會以並行方式執行。

官方文件:

為什麼這很方便?

你不需要自己建立、啟動和管理 threads。framework 幫你處理繁重的工作。你寫的程式看起來像普通迴圈,但底下自動處理並行化。

2. 語法:基礎範例

Parallel.For


long total = 0;
Parallel.For(1, 1_000_001, i =>
{
    // 這個 lambda 可能會被不同的 thread 同時執行
    Interlocked.Add(ref total, i * i); // 避免 race condition
});
Console.WriteLine($"平方和:{total}");

注意:我們透過 Interlocked.Add 更新變數 total,以避免資料競爭。

Parallel.ForEach


var numbers = Enumerable.Range(1, 10_000_000).ToArray();
long sum = 0;

Parallel.ForEach(numbers, num =>
{
    Interlocked.Add(ref sum, num * num); // 安全的加法
});
Console.WriteLine($"平方和:{sum}");

內部運作(視覺示意)


+-------------------+
|集合/範圍          |
+---------+---------+
          |
          v
  +----------------------+
  |   Parallel.ForEach   |
  +----------+-----------+
             |
        +----+----+----+----+
        |         |         |
        v         v         v
  Task #1    Task #2    Task #3   ... (可用核心)
        |         |         |
     +--+----+  +--+-----+  +--+-----+
     |處理中  |  |處理中  |  |處理中  |
     +-------+  +--------+  +--------+
        \         |         /
         +--------+--------+
                  |
                  v
               結果

3. 大檔案分析(CPU-bound 處理)

假設我們有一個包含數萬行的文字檔,每行都是一個數字。需要讀取檔案,把每個數字平方,並計算平方和。

同步版本


string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;

foreach (var line in lines)
{
    if (long.TryParse(line, out long n))
    {
        sum += n * n;
    }
}
Console.WriteLine($"平方和:{sum}");

使用 Parallel.For 的並行版本


string[] lines = File.ReadAllLines("numbers.txt");
long sum = 0;

Parallel.For(0, lines.Length, i =>
{
    if (long.TryParse(lines[i], out long n))
    {
        Interlocked.Add(ref sum, n * n);
    }
});
Console.WriteLine($"平方和:{sum}");

改變在哪:我們把普通迴圈換成了並行迴圈,並透過 Interlocked.Add 安全地增加 sum,避免多執行緒衝突。

4. 底層發生了什麼?

當你呼叫 Parallel.For 或 Parallel.ForEach 時,.NET 會自動把工作切成片段並分配到可用的處理器核心,使用 thread pool。每個片段會在自己的 thread 中獨立處理。

優點:如果你有 4 個核心,工作在理想情況下可以接近 4 倍加速(前提是任務不受外部資源或其他瓶頸限制,如記憶體或磁碟讀取速度)。

比較執行時間


var numbers = Enumerable.Range(1, 100_000_000).ToArray();
long sumSync = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();

foreach (var n in numbers)
    sumSync += n * n;

sw.Stop();
Console.WriteLine($"Sync: {sw.ElapsedMilliseconds} ms, 和:{sumSync}");

long sumParallel = 0;
sw.Restart();

Parallel.ForEach(numbers, n =>
    Interlocked.Add(ref sumParallel, n * n)
);

sw.Stop();
Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds} ms, 和:{sumParallel}");

自己試試看! 在高效能機器上,加速可能很明顯,但實際效果取決於任務特性與瓶頸。

5. 有用的細節

控制並行度

有時候限制使用的 threads 數是有意義的(例如避免把系統搞得太忙)。可以使用 MaxDegreeOfParallelism:


using System.Threading.Tasks;
long sum = 0;
var options = new ParallelOptions { 
    MaxDegreeOfParallelism = 2 
};
Parallel.For(0, 100, options, i =>
{
    Interlocked.Add(ref sum, i * i);
});
Console.WriteLine($"平方和:{sum}");

什麼情況下有用:如果部分計算主要負載在磁碟而不是 CPU,設定較少的 threads 可能會得到更好的整體效能。

何時該用並行迴圈

普通 for Parallel.For/Parallel.ForEach
處理器 使用單一核心 使用所有核心
順序性 有順序保證 無順序保證
速度 通常較慢 通常顯著較快
簡單性 非常簡單 需要考慮 thread-safe
最佳應用 小資料量、I/O-bound 大資料量、CPU-bound

延伸:Parallel 還能做什麼?

Parallel.Invoke() — 同時啟動多個獨立方法:


static void DoTask1() => Console.WriteLine("任務 1 完成");
static void DoTask2() => Console.WriteLine("任務 2 完成");
static void DoTask3() => Console.WriteLine("任務 3 完成");

Parallel.Invoke(
    () => DoTask1(),
    () => DoTask2(),
    () => DoTask3()
);

每個方法會盡可能在自己的核心上執行。

實務應用

  • 影像處理:同時處理不同區塊(例如套用濾鏡)。
  • 陣列計算:金融計算、模擬(例如不同情境的投資組合評估)。
  • 大規模日誌處理:在多核心上同時搜尋與彙總。
  • 機器學習:把工作切成獨立任務(資料 batch、feature engineering)。

而且在面試時你不只可以解釋什麼是並行迴圈,還能實事求是地評估它們的優缺點。

6. 在使用 Parallel.For 與 Parallel.ForEach 時常見錯誤

錯誤 #1:忽視資料競爭。
在沒有使用 Interlocked 或 lock 的情況下更新共享變數會導致錯誤結果,因為多個 thread 同時存取。

錯誤 #2:用在 I/O-bound 任務。
並行迴圈不會加速受磁碟或網路限制的任務,甚至可能因為額外開銷而變慢。

錯誤 #3:假設有執行順序。
並行迴圈不保證元素處理的順序,如果邏輯依賴順序,會出錯。

錯誤 #4:忽視副作用。
在並行迴圈中改變共用狀態(例如集合)會出問題,除非使用 thread-safe 的結構或同步機制。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION