.NET4.0并行计算技术基础(10)

 
 
今天贴出TPL的最后一部分内容,后面的小节将转去介绍PLINQ。
 
                                           金旭亮
                                       2009.10.17
============================================
 
 
 .NET4.0并行计算技术基础(10)
 
这是一个系列讲座,前面几讲的链接为:
 
.NET 4.0 并行计算技术基础(1)
.NET 4.0 并行计算技术基础(2)
. NET 4.0并行计算技术基础(3)
. NET4.0并行计算技术基础(4)
 .NET4.0并行计算技术基础(5)
.NET 4.0并行计算技术基础(6)
.NET4.0并行计算技术基础(7)
.NET4.0并行计算技术基础(8)
 .NET4.0并行计算技术基础(9)
=============================================
 

19.3.9 处理并行计算中的异常

         在这一小节中,我们来探讨一下如何并行计算中的异常捕获与处理问题。

1 处理并行循环中的异常

         在一个顺序执行的循环中,如果发生了一个异常,那么 .NET 的异常处理机制将会中止这个循环
 
    for(int i=0;i<1000000;i++)
    {
         // 串行代码:如果在此处发生异常,将导致循环的提前中止。
    }
 
         并行程序通常使用数据分区的手段,让多个线程并行执行同一个大循环的不同部分(比如将上述大循环份为 4 部分,用 4 个线程并行执行),这样一来,情况就变得复杂了。
         想象一下,假设在执行并行循环过程中,其中的一个线程发生了异常而终止,那么从理论上说,这意味着整个“大循环”已没有必要执行,因为继续执行下去有可能得到错误的结果。因此,必须让并行程序具备这样的一个机制: 一个线程中发生的异常,应该能传播给其他正在工作的线程。
         .NET 并行计算开发小组的工程师们在设计并行任务库时,采用了以下的设计方案:
         如果正在执行并行循环中某个“子循环”的线程发生了异常,它会阻止执行自己执行后继的循环代码(直接使用 .NET 标准的异常处理机制即可),然后这一情况通知任务并行库,任务并行库在得到通知以后,不会再创建新的线程执行并行循环的其他“子循环”,并负责将“有一个线程已经停止执行”这一事件通报给正在执行的其他线程。
         其他线程在接到此“情况通报”后,可以根据实际情况决定是“立即退出”,还是先完成一些清理工作再退出,具体时机是由软件工程师定的,他可以在设计并行循环的任务函数时,编写专门的代码来处理这一问题。
         当所有执行并行循环代码的线程停止之后,任务并行库收集所有相关线程所抛出的异常,合并成为一个 AggregateException 异常抛出。可以在启动此并行循环操作的“主线程 [1] ”中捕获此异常,从而得知工作没有能顺利执行完毕。
         对于那些由可能执行很长时间的子循环,为了避免因长时间等待其终止, Parallel.For Parallel.ForEach 都提供了特定的重载形式,用到了一个 ParallelLoopState 参数,例如:
 
public static ParallelLoopResult For(
    int fromInclusive, int toExclusive,
    Action<int, ParallelLoopState> body);
 
         ParallelLoopState 对象有一个 IsExceptional 属性,可以用于判断是否外部引发了一个异常,当其值为 true 时,表明属于同一并行循环的其他线程因为出现了未捕获的异常而终止, IsExceptional 属性由任务并行库负责设置。
         这就是任务并行库在不同线程间完成“情况通报”的基本方式。
         示例程序 HandleParallelLoopException 展示了如何处理并行循环中的异常( 1920 )。请读者自行阅读源码。

[1]   这里所说的“主线程”,不是指应用程序中的主线程,而是指负责启动并行循环的那个线程。
 
 
.NET4.0并行计算技术基础(10)_第1张图片
 

2 处理并行任务中引发的异常

         我们在前面介绍 Parallel 类时曾经指出,由此类启动的并行循环,在底层使用 Task 对象完成工作。因此,掌握如何处理并行任务引发的异常,更具有普遍的意义。
         当某个 Task 对象引发了一个未被捕获的异常时, TPL 会将此异常包装到一个特殊的 AggregateException 异常对象中。
         AggregateException 类的 InnerExceptions 属性包容了此轮并行代码中引发的所有异常。
         因此,一个典型的并行程序异常处理代码框架如下:
 
          try
            {
                 // 启动一个 Task 对象(取名 taskObject
                 taskObject.Start();
               // 等待其工作结束
                 taskObject.Wait();
            }
            catch (AggregateException ae)
            {
              // 处理并行代码中的异常
                foreach (Exception ex in ae.InnerExceptions)
                {
                    Console.WriteLine("{0}:{1}",ex.GetType(),ex.Message);
                }
            }
 
         情况比较复杂的是,任务是可以嵌套的。比如一个并行任务可能会创建多个子任务,而这些子任务又会创建更多的“孙子任务”,由此构成一个任务对象的树型结构。
         当这棵“任务对象树”中的任何一个节点(即任务对象自身)引发了一个未捕获的异常时, TPL 都会为此任务对象创建一个 AggregateException 对象,把前述那个未捕获的异常对象添加到创建好的 AggregateException 对象的 InnerExceptions 集合中,然后,再把这一个 AggregateException 对象添加到其父任务对象所关联的 AggregateException 对象的 InnerExceptions 集合中(这段话比较拗口,请读者仔细阅读)。
         这样一来,任务对象的嵌套就导致了 AggregateException 对象的嵌套,而这种嵌套还是递归进行的,这就给编写异常处理代码带来了麻烦,你必须“下钻”到 AggregateException 对象树的最底层才能得到真正的异常对象,以下是访问两层“异常树”的示例代码:
 
           catch (AggregateException ae)
            {
              // 处理并行代码中的异常
                foreach (Exception ex in ae.InnerExceptions)
                {
                   // “下钻”一层,处理子任务引发的异常
                    if(ex is AggregateException)
                 {
                         foreach(Exception innerEx in ex. InnerExceptions)
                      { // …… ( 代码略 ) }
                    }
                    else // 本任务引发的异常
                        Console.WriteLine("{0}:{1}",ex.GetType(),ex.Message);
                }
            }
 
         为了解决需要“递归”编写异常处理的问题, AggregateException 类提供了一个将多层的 AggregateException 对象树“展平”为一层的方法―― Flatten() 。使用此方法时,异常处理代码不需要遍历 AggregateException 对象树:
 
    catch (AggregateException ae)
    {
        ae. Flatten();
        // 处理并行代码中的异常
        foreach (Exception ex in ae.InnerExceptions)
        {
            // 异常处理代码……
        }
    }
 
         经过“展平”之后,AggregateException. InnerExceptions 将只包容具体的异常对象,不再包容嵌套的AggregateException 对象。

3 屏蔽掉特定的异常

         在特定场景下,我们可以在任务中直接处理或忽略掉某种特定种类的异常,这时,肯定不需要将这些异常对象加入到 AggregateException 对象中。
         AggregateException 类提供了一个 Handle() 方法来实现这一目的。
 
    public void Handle(
         Func<Exception, bool> predicate
    )
 
         predicate 参数引用一个函数, TPL 会对任务所引发的每个异常都调用一次这个函数。这个函数的参数就是具体的异常对象。当此函数返回 True 时,表示此异常对象已经被处理过了,所以,不会被包装到 AggregateException 对象中。
         例如,以下代码将“吃掉” DivideByZeroException 异常:
 
    catch (AggregateException ae)
    {
        ae.Handle((ex) =>
                {
                    if (ex is DivideByZeroException)
                                    return true;
                    else
                                    return false;
                });
        throw ae;
    }
 

4 并行任务异常处理示例

         我们用一个综合的实例 HandleTaskException 来展示并行任务异常处理的编程技巧。
         此示例项目定义了 3 个子任务,每个任务分别引发一种特定的异常。然后,再创建一个“父”任务,在“父”任务中启动这 3 个子任务。在 Main() 函数中捕获所有并行任务引发的异常。
         请读者仔细阅读示例源码,在本节所介绍的基本原理的指导下,依据代码中的说明进行动手实验,掌握捕获并处理并行任务异常的基本编程方法。为节省篇幅,本书不再赘述示例的技术细节。
 
==================================================
 
下一讲,介绍PLINQ编程技术。
 
  《.NET4.0并行计算技术基础(11)》
 

你可能感兴趣的:(.net,代码,职场,休闲,行计算)