CodeGym /課程 /C# SELF /Parallel.For

Parallel.ForParallel.ForEach 中的例外

C# SELF
等級 61 , 課堂 2
開放

1. 在 Parallel.ForParallel.ForEach 中例外的運作

在普通的 for 迴圈裡很簡單:如果在迴圈體內丟出例外 — 迴圈立刻結束,例外會往外拋。在平行迴圈裡情況不同。我們來拆解。

所有例外都會被收集到一個「包」裡

當平行迭代(Parallel.For/ForEach)的某一次執行中發生例外時,它不會立即向外拋出,而是被打包。這個過程會繼續:其他迭代要麼完成,要麼也丟出例外。結果是:當平行迴圈結束執行(或被強制中斷)時,所有「丟出的」例外會被收集並以一個 AggregateException 物件一起拋出。

AggregateException 是一個「容器」,裡面存放了在平行迭代期間發生的所有例外集合。這很方便:我們可以一次拿到所有錯誤(或者至少在主要執行緒完成前收集到的那些錯誤)。

實務上的樣子

範例:平行處理,偶爾會丟出例外

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 0, 4, 0, 6, 7, 8 };

        try
        {
            Parallel.ForEach(numbers, number =>
            {
                // 我們故意除以這個數,有時候它等於零!
                // 這會引發 DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("在平行迴圈中發現錯誤!");

            // 遍歷所有發生的例外
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"類型: {inner.GetType().Name} — 訊息: {inner.Message}");
            }
        }
    }
}

會發生的情況:

  • 主集合裡有零,而除以 0 在數學(和 C#)裡是禁區:會產生 DivideByZeroException
  • 平行迴圈開始處理。一旦某處發生除以零 — 迴圈不會立刻停止,而是讓已經開始執行的迭代繼續執行。
  • 當所有執行緒完成工作(有的發生錯誤、有的沒有)後,外層會拋出一個包含所有發生例外的 AggregateException

把例外處理機制視覺化

flowchart LR
    A[執行緒 1]
    B[執行緒 2]
    C[執行緒 3]
    D[執行緒 4]
    E[Parallel.ForEach]
    F[例外 1]
    G[例外 2]
    H[AggregateException]
    subgraph 迭代
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

圖中可見:不同執行緒可能遇到不同錯誤,最終它們都會被「打包」成單一的 AggregateException

2. 錯誤處理的實務要點

如何處理 AggregateException

當捕捉到 AggregateException 時,通常有兩種情境:

  • 把所有錯誤顯示給使用者(或寫到日誌),以便累積經驗。
  • 判斷哪些錯誤是關鍵的,哪些是雜訊:決定整個操作要不要算失敗,或許忽略某些錯誤。

常見模式:透過 Handle 來處理

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"錯誤在迭代 {i}");
        Console.WriteLine($"已處理: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("捕捉到錯誤: " + e.Message);
            // true = 錯誤被視為已處理
            return true;
        }
        // false = 不處理,會再拋出
        return false;
    });
}

這種做法允許你只處理那些你認為是「正常」的錯誤,其他的則繼續往上傳遞,以免忽略真正的重大故障。

實作上的有趣(且危險)細節

迴圈何時停止?
當某次迭代發生例外時,Parallel.For/ForEach 不會再啟動新的迭代,但已經開始執行的迭代會繼續執行。等所有活動中的迭代結束後,會拋出 AggregateException。如果執行緒很多,仍會有「尾巴工作」完成——因此可能會有多個錯誤被收集。

如果不捕捉例外,應用會當掉。
如果不把 Parallel.For/ForEach 包在 try-catch 裡,應用會在所有迭代完成後於第一次遇到的錯誤位置崩潰——這對使用者來說很不友善。

把例外「留在」迴圈內處理。
有時你想要讓個別迭代的錯誤不破壞整體流程,那就直接在平行迴圈體內捕捉例外:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"在數字 {number} 發生錯誤: {ex.Message}");
    }
});

這種方式適合你不需要把所有例外一次性收集的情境——你在出錯時立即處理(例如寫日誌)。但要小心:如果這麼做了,就不會產生 AggregateException,也就無法從外部一次性知道整體是否完全正常。

當呼叫 Break()Stop() 時。
如果迭代呼叫了 ParallelLoopState.Break()ParallelLoopState.Stop(),迴圈會嘗試停止新的迭代:Break() 在當前索引之後結束迭代,而 Stop() 會停止所有迭代。然而如果同時發生例外,例外會被保留並在所有活動迭代結束後以 AggregateException 拋出。

3. 有用的細節

普通迴圈 vs 平行迴圈的例外

在普通迴圈中,任何錯誤都會導致工作立即結束:例外向外拋出,一切停止。

在 C# 的平行迴圈中採取比較折衷的策略:已啟動的任務會繼續執行,只有在整個過程完成後才把所有錯誤一次性「吐出」。這讓你能夠收集到所有錯誤,而不會遺失其中的任何一個,然後在迴圈結束後再做決策。

4. 在 Parallel.ForParallel.ForEach 處理例外時常見的錯誤

錯誤 №1:忽略 AggregateException
如果不捕捉 AggregateException,應用會在所有迭代結束後當掉,可能導致資料遺失,並在伺服器或 GUI 應用中造成故障。

錯誤 №2:在沒有 try-catch 的情況下呼叫 .Wait()
Parallel.For/ForEach 呼叫 .Wait() 而不處理 AggregateException 會導致未處理的例外,讓診斷變得困難。

錯誤 №3:忽視重複錯誤。
多個相同的錯誤(例如多次除以零)可能是由重複的資料引起。若不分析 InnerExceptions,可能會忽略根本原因。

錯誤 №4:吞掉所有例外。
在迴圈內使用 catch (Exception) { /* 空 */ } 會隱藏錯誤,導致重要資訊遺失與「幽靈」型 bug。

不同迴圈中錯誤行為比較

選項 普通 for/foreach Parallel.For / ForEach
例外被處理的時間 立即 在所有迭代完成後
錯誤格式 單一 exception 包含集合的 AggregateException
其它迭代 不再執行 已啟動的繼續執行
在迴圈體內捕捉錯誤 可以 可以
在外部捕捉錯誤 可以 可以,透過 AggregateException

要點與面試短問:

  • 如果不處理 AggregateException 會怎樣?
    應用會在所有迭代結束後崩潰——不論錯誤發生在何時或何處。
  • 能否知道錯誤發生在哪一個迭代?
    只有你在例外裡加入索引或資料資訊時才知道。
  • AggregateException 可能是空的嗎?
    不會,只有當至少有一個內部例外時它才會被建立。如果沒有錯誤,就不會拋出它。
  • 如果在迴圈內捕捉例外,外面還會有錯誤嗎?
    不會,因為你已經在內部處理了,外部就不會再收到 AggregateException

現在你不僅能啟動多執行緒的迴圈,還能優雅地處理它們的平行「故障」!如同一貫提醒:對多執行緒要小心,它喜歡驚喜,尤其是那些沒有人處理的。

2
任務
C# SELF, 等級 61, 課堂 2
上鎖
Parallel.For 的簡單用法範例,含例外處理
Parallel.For 的簡單用法範例,含例外處理
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION