多线程与并行编程尤其要注意异常的处理,如果不处理后台任务中的异常,应用可能就会莫名其妙的退出,影响用户体验。
如何处理非主线程中的异常,办法就是将其包装到主线程中去。接下来的代码演示的就是将非主线程中的异常包装到主线程中。
static void Main(string[] args) { Task t = new Task(() => {
//看看主线程是否会捕获到该异常 throw new Exception("未知异常..."); }); t.Start(); try { //等待Task完成,若有Result,可求Result t.Wait(); } catch (AggregateException e)//AggregateException表示程序执行期间发生一个或多个异常 { foreach (Exception item in e.InnerExceptions) { Console.WriteLine("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", item.GetType(), Environment.NewLine, item.Source, Environment.NewLine, item.Message); } } Console.WriteLine("主线程马上结束..."); Console.ReadKey(); }
在任务并行库中,可对任务进行Wait、WaitAny、WaitAll方法,或者求Reault属性。这时候对方法进行异常捕获,可以做到将非主线程中的异常包装到主线程中。
值得一提的是,上述方法虽可捕获非主线程的异常,但是这会阻滞当前线程,为解决这样的问题,需考虑任务并行库中Task类型的一个功能:新起一个后续任务,以解决等待问题。
static void Main(string[] args) { Task t = new Task(() => { throw new Exception("未知异常..."); }); t.Start(); Task tEnd = t.ContinueWith((task) => { foreach (Exception item in task.Exception.InnerExceptions) { Console.WriteLine("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", item.GetType(), Environment.NewLine, item.Source, Environment.NewLine, item.Message); } }, TaskContinuationOptions.OnlyOnFaulted);//OnlyOnFaulted:指定只应在延续任务前面的任务引发
//了未处理异常的情况下才安排延续任务,此任务对多任务延续无效 Console.WriteLine("主线程马上结束..."); Console.ReadKey(); }
以上方法解决了主线程等待的问题,但异常处理其实却并没有回到主线程中,它依然在线程池里。如何进一步将异常处理封装到主线程中去呢。接下来解决异常和异常处理封装到主线程问题。
static void Main(string[] args) { Task t = new Task(() => { throw new Exception("未知异常..."); }); t.Start(); Task tEnd = t.ContinueWith((task) => { throw task.Exception; }, TaskContinuationOptions.OnlyOnFaulted); try { tEnd.Wait(); } catch (AggregateException e) { foreach (Exception item in e.InnerExceptions) { Console.WriteLine("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", item.GetType(), Environment.NewLine, item.Source, Environment.NewLine, item.Message); } } Console.WriteLine("主线程马上结束..."); Console.ReadKey(); }
仔细的观察其实你会发现,在一开始的代码中对主工作任务采用Wait的方法是不可取的,因为可能主工作任务可能要执行很长一段时间,这对调用者造成阻塞,会觉得很难忍受。第二段代码是将异常处理专门放入一个新任务中,新任务只负责捕获异常,这其实并未封装异常到主线程。最后我们可以得到第三段更为合理的代码并行一个新任务专门捕获异常,将处理异常的任务Wait方法放入主工作任务,捕获异常的过程在新任务中既不会阻塞主线程,主线程也可以捕获到并行线程的异常,一举两得。
对线程调用Wait方法会阻塞主线程,并且CLR要为它在后台新起线程池线程来完成额外的工作。将异常封装到主线程其实有个更好的方法:事件通知。
建议大家使用事件通知的模型处理Task中的异常。
//事件委托
static event EventHandler<AggregateExceptionArgs> AggregateExceptionCatched; static void Main(string[] args) { AggregateExceptionCatched += new EventHandler<AggregateExceptionArgs>(Program_AggregateExceptionCatched); Task t = new Task(() => { try { throw new InvalidOperationException("任务并行编码中产生未知异常"); } catch (Exception err) { AggregateExceptionArgs errArgs = new AggregateExceptionArgs() { ProAggregateException = new AggregateException(err) }; AggregateExceptionCatched(null, errArgs); } }); t.Start(); Console.WriteLine("主线程马上结束..."); Console.ReadKey(); } static void Program_AggregateExceptionCatched(object sender, AggregateExceptionArgs e) { foreach (var item in e.ProAggregateException.InnerExceptions) { Console.WriteLine("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", item.GetType(), Environment.NewLine, item.Source, Environment.NewLine, item.Message); } }
//自定义事件类 public class AggregateExceptionArgs : EventArgs { public AggregateException ProAggregateException { get; set; } }
扩展:任务调度器(待续)
任务调度器TaskScheduler有一个静态事件用于处理未捕获到的异常。不过一般是不建议使用,因为事件回调是在进行垃圾回收的时候才发生的。
private static void TaskScheduleMethod() { //TaskScheduler标识一个处理将任务排队到线程中的低级工作对象 TaskScheduler.UnobservedTaskException += new EventHandler<UnobservedTaskExceptionEventArgs>(TaskScheduler_UnobservedTaskException); Task t = new Task(() => { throw new Exception("执行并行任务时产生的位置异常"); }); t.Start(); Console.ReadKey(); t.Dispose(); t = null; GC.Collect(); Console.WriteLine("主线程马上结束..."); Console.ReadKey(); } static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { foreach (var item in e.Exception.InnerExceptions) { Console.WriteLine("异常类型:{0}{1}来自:{2}{3}异常内容:{4}", item.GetType(), Environment.NewLine, item.Source, Environment.NewLine, item.Message); }
//将异常标识为已经观察到
e.SetObserved(); }
上述代码如果将GC.Collect()注释掉,则不会输出异常信息,因为发生异常的时候并没有发生垃圾回收(垃圾回收时机有CLR决定),必须将该注释取消,强行执行垃圾回收才能观察到异常信息,这也正是任务调度器的局限性。