隐藏

多线程之旅(8)_Task的异常捕获和处理方法——附C#源码

发布:2021/2/2 16:10:32作者:管理员 来源:本站 浏览次数:1471

我们的线程一旦大量(高并发)且有序(线程同步安全)的起飞后,我们就要关心如何优雅的处理线程异常了。
在线程内部写try,catch当然是可以的,但这种捕获方法就使得我们无法在外部主进程中很好的对线程异常进行控制,只能随他自生自灭。所以我们主要讨论一下在线程外部如何捕获线程的异常信息。
线程异常的捕获方法有阻塞型捕获和异步捕获两种。
阻塞型捕获异常就是:当一个或多个线程启动后,我们就一直等待,等到所有线程执行完成,判断这些线程中是否出现异常,而后再执行后续的代码。
异步型捕获异常就是:一个线程启动后,我们不去等待他执行完成,而知直接执行后续其他代码。当线程出现异常时,会自动返回异常信息,或者在需要时主动去获取线程异常信息。
下面我们来看看如何操作。

GitHub源码地址:https://github.com/yangwohenmai/TEST/tree/master/TaskException

一.通过wait方法捕获线程中的异常(线程阻塞方式)

通过t.Wait();的方式,可以对线程异常进行捕获,但此时线程的执行会阻塞,不会再有异步的效果。

catch到异常的时候,通过catch(AggregateException  e){}获取到task各自的异常信息

System.AggregateException:  已取消 -或- 在 System.Threading.Tasks.Task 的执行期间引发了异常。如果任务已被取消,System.AggregateException将包含其 System.AggregateException.InnerExceptions 集合中的 System.OperationCanceledException。

AggregateException  其本质是对Exception  的继承,所以这里用catch(Exception  e){}也可以


	
  1. #region 通过Wait捕获异常
  2. /// <summary>
  3. /// 通过wait可以捕获Task内部的异常
  4. /// </summary>
  5. public static void WaitException()
  6. {
  7. try
  8. {
  9. //和线程不同,Task中抛出的异常可以捕获,但是也不是直接捕获,而是由调用Wait()方法或者访问Result属性的时候,由他们获得异常,将这个异常包装成AggregateException类型,或者直接以Exception,抛出捕获。
  10. //默认情况下,Task任务是由线程池线程异步执行。要知道Task任务的是否完成,可以通过task.IsCompleted属性获得,也可以使用task.Wait来等待Task完成。
  11. Task t = Task.Run(() => TestException());
  12. t.Wait();
  13. }
  14. catch (Exception ex)
  15. {
  16. var a = ex.Message; //a的值为:发生一个或多个错误。
  17. var b = ex.GetBaseException(); //b的值为:Task异常测试
  18. Console.WriteLine(a + "|*|" + b);
  19. }
  20. }
  21. static void TestException()
  22. {
  23. throw new Exception("Task异常测试");
  24. }
  25. #endregion

二.通过 WaitAll捕获线程异常(线程阻塞方式)

通过Task.WaitAll(task1, task2, task3);的方式,可以对WaitAll内包含的所有的线程异常进行捕获。

catch到异常的时候,通过遍历异常集合AggregateException,可以获取到每个task各自的异常信息


	
  1. #region 用WaitAll可以通过catch获取WaitAll等待的,所有线程的,内部的异常
  2. public static void WaitAllException()
  3. {
  4. try
  5. {
  6. HttpClient hc = new HttpClient();
  7. var task1 = hc.GetStringAsync("http://www.1111.com");
  8. var task2 = hc.GetStringAsync("http://www.2222.com");
  9. var task3 = Task.Run(() => { int a = 1, b = 0; int c = a / b; });
  10. //只有WaitAll可以通过AggregateException捕获程序内部所有异常,WaitAny,WhenAll,WhenAny都不能捕获
  11. Task.WaitAll(task1, task2, task3);
  12. Console.WriteLine("执行成功");
  13. }
  14. //AggregateException类表示应用程序执行期间发生的一个或多个错误。(即:所有的内部异常)
  15. catch (AggregateException ae)
  16. {
  17. //InnerExceptions表示获取导致当前异常的System.Exception实例的只读集合。
  18. foreach (Exception item in ae.InnerExceptions)
  19. {
  20. Console.WriteLine(item.Message);
  21. }
  22. Console.ReadKey();
  23. }
  24. }
  25. #endregion

三.通过ContinueWith设置TaskContinuationOptions参数来捕获异常(线程异步方式)(推荐)

ContinueWith有一个很好用的捕获异常的小技巧,就是通过设置TaskContinuationOptions参数。

将ContinueWith的第二个参数设置成TaskContinuationOptions.OnlyOnFaulted,此时ContinueWith内部的代码只有在Task出现异常后才会执行,如果Task没有异常,则ContinueWith内部的方法不会执行。因此我们只需要在ContinueWith内部写好异常处理方法就行了。

值得一提的是,这个方法是不会阻塞进程,能很好的满足异步需求。


	
  1. #region 通过ContinueWith设置TaskContinuationOptions参数来捕获异常(推荐)
  2. public static void ContinueWithException(int x, int y)
  3. {
  4. Task<string> t = Task.Run<string>(() =>
  5. {
  6. Thread.Sleep(3000);
  7. Console.WriteLine("我是线程还在异步执行");
  8. return Sumt(x, y).ToString();
  9. });
  10. //NotOnFaulted表示如果没有异常,才会执行ContinueWith内部的代码,但此时线程不会阻塞
  11. //t.ContinueWith(r =>
  12. //{
  13. // string Exception = Convert.ToString(t.Exception);
  14. // Console.WriteLine("异常信息1:" + Exception);
  15. //}, TaskContinuationOptions.NotOnFaulted);
  16. //Console.WriteLine("继续异步执行1");
  17. //OnlyOnFaulted表示如果有异常,才会执行ContinueWith内部的代码,但此时线程不会被阻塞
  18. t.ContinueWith(r =>
  19. {
  20. //Thread.Sleep(3000);
  21. string Exception = Convert.ToString(t.Exception);
  22. Console.WriteLine("异常信息2:" + Exception);
  23. }, TaskContinuationOptions.OnlyOnFaulted);
  24. Console.WriteLine("继续异步执行2");
  25. //askContinuationOptions.OnlyOnFaulted表示:指定只应在延续任务前面的任务引发了未处理异常的情况下才安排延续任务。 此选项对多任务延续无效。【即:只有在发生异常的情况下才将异常信息记录到日志】
  26. }
  27. private static int Sumt(int x, int y)
  28. {
  29. return x / y;
  30. }
  31. #endregion

执行结果如下,可以从图中看出,线程并没有发生阻塞,在线程出现异常后, ContinueWith内部的方法打印出了异常信息。

四. 通过t.Exception在线程执行完之后去判断异常信息(线程异步方式)

如果希望程序保持异步执行,而非阻塞等待,我们可以在一个线程执行一段时间后,通过对线程的返回结果进行判断,从而去获取异常信息。

(1)我们可以先通过IsCompleted == true去判断线程运行是否结束

(2)如果线程结束后我们可以通过t.Exception != null去判断线程执行过程中是否出现异常

(3)如果有异常我们可以通过t.Exception.InnerExceptions.Count去判断异常数量

(4)最后t.Exception.InnerException.Message获取异常信息即可


	
  1. #region 对于线程,外层的catch抓不住线程的报错,通过t.Exception对象获取线程的异常
  2. public static void GetException(int x, int y)
  3. {
  4. Task<string> t = Task.Run<string>(() => { return Sum(x, y).ToString(); });
  5. Thread.Sleep(8000);
  6. //t.ContinueWith(r => { Console.WriteLine("异常信息:" + t.Exception.InnerException.Message); });
  7. if (t.IsCompleted == true)
  8. {
  9. if (t.Exception != null)//Exception对象不为null
  10. if (t.Exception.InnerExceptions.Count > 0)//内部异常信息数量大于0
  11. if (!string.IsNullOrEmpty(t.Exception.Message))//ErrorMessage不为空
  12. Console.WriteLine("异常信息:" + t.Exception.InnerException.Message);
  13. }
  14. }
  15. private static int Sum(int x, int y)
  16. {
  17. return x / y;
  18. }
  19. #endregion

源码地址见文首链接