CodeGym /课程 /C# SELF /Parallel.For 和 Parallel.For...

Parallel.For 和 Parallel.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 也不会出现。

现在你不仅能跑多线程循环,也能巧妙处理它们的“并发事故”了!记住,多线程喜欢出惊喜,尤其是当没人处理这些异常时。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION