Task类在.NET4.5中的一些改进

简介:

Task类在.NET4.5中,做了一些改进,比如新增了方法ConfigureAwait,Delay,Run等方法。其中一个重要修改,就是对于异常的处理。

在.NET4.0中,Task中抛出的异常,如果没有去捕获,在Task被垃圾回收的时候,析构函数检测到该Task对象还有未被处理过的异常,会抛出这个异常,并且导致进程终结,进程终结的时间是由垃圾回收器和析构方法决定的。(可以通过注册TaskSchedular.UnobservedTaskException事件处理未捕获的异常。)

ThrowUnobservedTaskExceptions节点

在.NET4.5中,微软改变了策略,对于task中未处理的异常,默认情况下不会导致杀死进程。这个可以通过配置ThrowUnobservedTaskExceptions节点实现。

默认情况下,ThrowUnobservedTaskExceptions这个节点的enabled=false。如下配置。

<configuration>
    <runtime>
      <ThrowUnobservedTaskExceptions enabled="false"/>
    </runtime>
</configuration>

这种情况下,在.NET4.5中,GC回收对象的时候,是不会导致程序崩溃的,未捕获的异常就这样消失了。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static  void  Main( string [] args)
{
       for  ( int  i = 0; i < 10; i++)
       {
            var  t = Task.Factory.StartNew< int >(() => {  throw  new  Exception( "xxxxxx" );  return  1; }
                         , CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
             }
       while  ( true )
      {
            GC.Collect();
            Thread.Sleep(1000);
       }
       Console.ReadKey();
}

但是如果设置<ThrowUnobservedTaskExceptions enabled="true"/>,那么程序就会崩溃,这和在.NET4.0中的结局一样。

spacer.gif

wKioL1P1rrGQElg4AAMGwpRKbdU957.jpg

当然,对于所有的异常,都是建议捕获并且处理的。.NET4.0和4.5都提供了TaskScheduler.UnobservedTaskException事件,通过监听这个事件,也可以捕获到这个异常。

代码如下:

1
2
3
4
5
6
TaskScheduler.UnobservedTaskException += (o, ev) =>
{
       Console.WriteLine(ev.Exception);
       ev.SetObserved();
       Console.WriteLine( "---------" );
};

注意:ev.SetObserved();方法必须要调用,这样才能阻止进程崩溃。

 

.NET4.0中Task的异常

还有一处改进是对于异常的捕获上。对于Task中抛出的异常,外部的try catch是无法捕获的。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
static  Task< int > f()
{
try
{
   var  t = Task.Factory.StartNew< int >(() => {  throw  new  Exception( "xxxxxx" );  return  1; });
   return  t;
}
catch
{
   Console.WriteLine( "error" );
}
return  null ;
}

无论是在.net4.0还是.net4.5,上述代码都是不会走到catch中的。只有当运行Task.Wait或者读取Task.Result的时候(这些方法都会引起阻塞),才会抛出异常,由于运行的Task可能是包含了多个子Task,或者在WaitAll多个Task,那么异常可能会出现多个。Task类会把这些异常包成AggregateException异常。要获得异常的正真信息,需要访问AggregateException.InnerExceptions属性。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Task< int > t1 = Task.Factory.StartNew< int >(() =>
   {
    throw  new  Exception( "error1" );
   });
Task< int > t2 = Task.Factory.StartNew< int >(() =>
{
throw  new  Exception( "error2" );
});
Task< int > t3 = Task.Factory.StartNew< int >(() =>
{
throw  new  Exception( "error3" );
});
try
{
Task.WaitAll(t1,t2,t3);
}
catch  (AggregateException ex)
{
Console.WriteLine( "Exception Type:{0}" , ex.GetType());
Console.WriteLine( "Exception Message:{0}" , ex.Message);
Console.WriteLine( "Exception StackTrace:{0}" , ex.StackTrace);
Console.WriteLine( "Exception InnerException.Message:{0}" , ex.InnerException.Message);
foreach  ( var  innerEx  in  ex.InnerExceptions)
{
   Console.WriteLine( "InnerExceptions.Message:{0}" , innerEx.Message);
}
}

Task.WaitAll三个Task,捕获异常的时候,类型为AggregateException,并且可以通过InnerExceptions,遍历出每一个异常。这里值得注意的是,如果AggregateException的InnerExceptions有3个异常的话,AggregateException的InnerException会抛出哪个异常?根据老赵的说法,C#开发团队“故意”不提供文档说明究竟会抛出哪个异常。因为他们并不想做出这方面的约束,因为这部分行为一旦写入文档,便成为一个规定和限制,为了类库的兼容性今后也无法对此做出修改。

上面代码的输出如下:

spacer.gifwKiom1P1rZmA-a4BAAG0CLdly_E714.jpg

 

async/await

在.net 4.5中,c#5.0的语法支持了async/await的写法,这种写法下,可以按照同步的思路写异步方法。而且,异常处理也变得可以直接捕获,在await的时候,代码可以直接捕获异常,例如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static  void  Main( string [] args)
{
     var  t = f();
     Console.ReadKey();
}
async  static  Task< int > f()
{
     int  val = 0;
     try
     {
         val = await Task.Factory.StartNew< int >(() => {  throw  new  NotSupportedException( "xxxxxx" );  return  1; });
     }
     catch  (Exception ex)
     {
         Console.WriteLine( "Exception Type:{0}" , ex.GetType());
         Console.WriteLine( "Exception Message:{0}" , ex.Message);
         Console.WriteLine( "Exception StackTrace:{0}" , ex.StackTrace);
     }
     return  val;
}

输出如下:

spacer.gifwKiom1P1rZrxgCwJAAH3iV5aHI8366.jpg

注意,可以看到错误的堆栈信息。从System.Threading.Tasks.Task类切换到了 System.Runtime.CompilerServices.TaskAwaiter。

C# 使用了SynchronizationContext类完成了这个切换。当await一个Task的时候,当前的 SynchronizationContext对象被存储下来。当方法继续向下运行的时候,await关键字的结构使用Post方法,在之前保存的SynchronizationContext类的基础上,继续运行该方法。更多细节不在这里描述。因此只要await关键字的方法上出现了异常,就可以捕获掉,并且捕获的异常类型,就可以不把异常往上层抛,还有一个不同点是await方法捕获的异常类型就是直接的类型,而不是System.AggregateException。

但是如果在await等待是多个Task,并且这多个Task都抛出了异常,那么,最终捕获的异常也会是System.AggregateException类型。并且也可以遍历出所有的异常。和之前的处理同步情况下是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static  void  Main( string [] args)
{
var  t = f();
Console.WriteLine(t.Result);
Console.ReadKey();
}
async  static  Task< int > f()
{
int  val = 0;
Task< int []> all =  null ;
try
{
   Task< int > t1 = Task.Factory.StartNew< int >(() =>
   {
    throw  new  NotImplementedException( "error1" );
    return  1;
   });
   Task< int > t2 = Task.Factory.StartNew< int >(() =>
   {
    throw  new  NotImplementedException( "error2" );
    return  2;
   });
   Task< int > t3 = Task.Factory.StartNew< int >(() =>
   {
    throw  new  NotImplementedException( "error3" );
    return  3;
   });
await (all = Task.WhenAll(t1, t2, t3));
   val = all.Result.Sum();
}
catch  (Exception ex)
{
   Console.WriteLine( "Exception Type:{0}" , ex.GetType());
   Console.WriteLine( "Exception Message:{0}" , ex.Message);
   Console.WriteLine( "Exception StackTrace:{0}" , ex.StackTrace);
   foreach  ( var  innerEx  in  all.Exception.InnerExceptions)
   {
    Console.WriteLine( "InnerExceptions.Message:{0}" , innerEx.Message);
   }
}
return  val;
}

输出如下:

spacer.gifwKioL1P1rrKCaSAUAAJAzcfdhh8019.jpg

上述代码中,使用了WhenAll方法,返回的是一个Task类。由于t1,t2,t3中都包含了异常,因此返回的Task中有3个异常,但await关键字只会允许抛出一个具体的异常,因此,此处抛出了第一个异常。通过对all变量的遍历,可以得到所有的异常。


WhenAll 方法

WhenAll方法是.NET4.5中新增的方法。它的返回值是一个Task,仅当所有的Task都完成的时候,返回的这个Task才算完成,并且返回值可以是各个task返回值的一个数组。

因此可以把WhenAll方法看成是一组Task的合并。WhenAll和WaitAll有点类似,但本质上很多不同。

1.调用Task.WaitAll的时候,会阻塞当前线程,直到所有的Task都完成了。而Task.WhenAll方法不会阻塞当前线程,而是直接返回了一个Task,只有在读取这个Task的Result的时候,才会引起阻塞。

2.WaitAll的各类重载方法,它们的返回值是void或者bool,而WhenAll的返回值是Task。因此WhenAll方法更好的支持async/await异步写法。

下面代码演示WhenAll方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static  void  Main( string [] args)
{
Task< int > t1 = Task.Factory.StartNew< int >(() =>
{
   Thread.Sleep(1000);
   return  1;
});
Task< int > t2 = Task.Factory.StartNew< int >(() =>
{
   Thread.Sleep(2000);
   return  2;
});
Task< int > t3 = Task.Factory.StartNew< int >(() =>
{
   Thread.Sleep(3000);
   return  3;
});
var  all = Task.WhenAll(t1, t2, t3);
while  ( true )
{
   Console.WriteLine( "{0} IsCompleted:{1}" , DateTime.Now.ToString( "HH:mm:ss fff" ), all.IsCompleted);
   Thread.Sleep(200);
   if  (all.IsCompleted)
    break ;
}
var  c = all.Result;
Console.WriteLine(c.Sum());
}

输出的结果为:

spacer.gifwKiom1P1rZrzGcV_AAFgWd3KvUo280.jpg

ConfigureAwait方法

ConfigureAwait方法的作用是指定代码在执行await操作的时候,是否捕获上下文,捕获上下文会带来性能开销。不捕获上下文,在ASP.NET或者GUI程序时,可能带来问题。更多的细节可以参考Stephen Cleary 的异步编程中的最佳做法

总之,.NET4.5中的Task的一些改进,都是为了迎合异步async/await的写法而做出的改进。更多参考资料:

http://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx

http://msdn.microsoft.com/zh-cn/magazine/gg598924.aspx

以及Stephen Cleary的blog:http://blog.stephencleary.com/


















本文转自cnn23711151CTO博客,原文链接:http://blog.51cto.com/cnn237111/1542703 ,如需转载请自行联系原作者






相关文章
|
4月前
|
算法 Java 调度
|
7月前
|
IDE API 开发工具
拦截|篡改|伪造.NET类库中不限于public的类和方法
本文除了回顾拦截.NET类库中的方法,实现方法参数的篡改、方法返回结果的伪造,再着重介绍.NET类库中非public类及方法如何拦截。
拦截|篡改|伪造.NET类库中不限于public的类和方法
|
10月前
|
C#
.NET Core反射获取带有自定义特性的类,通过依赖注入根据Attribute元数据信息调用对应的方法
.NET Core反射获取带有自定义特性的类,通过依赖注入根据Attribute元数据信息调用对应的方法
122 0
|
开发框架 JSON 前端开发
【C#】.net core2.1,自定义全局类对API接口和视图页面产生的异常统一处理
在开发一个网站项目时,异常处理和过滤功能是最基础的模块 本篇文章就来讲讲,如何自定义全局异常类来统一处理
204 0
|
JSON 数据格式
【.NET开发福音】使用Visual Studio将JSON格式数据自动转化为对应的类
【.NET开发福音】使用Visual Studio将JSON格式数据自动转化为对应的类
533 0
【.NET开发福音】使用Visual Studio将JSON格式数据自动转化为对应的类
|
缓存 移动开发 C#
【.Net实用方法总结】 整理并总结System.IO中TextWriter类及其方法介绍
本文主要介绍System.IO命名空间的TextWriter类,介绍其常用的方法和示例说明。
|
C# 开发者 索引
【.Net实用方法总结】 整理并总结System.IO中TextReader类及其方法介绍
本文主要介绍System.IO命名空间的TextReader类,介绍其常用的方法和示例说明。
|
存储 缓存 C#
【.Net实用方法总结】 整理并总结System.IO中StringWriter类及其方法介绍
本文主要介绍System.IO命名空间的StringWriter类,介绍其常用的方法和示例说明。
|
C# 开发者 索引
【.Net实用方法总结】 整理并总结System.IO中StringReader类及其方法介绍
本文主要介绍System.IO命名空间的StringReader类,介绍其常用的方法和示例说明。
|
存储 网络协议 程序员
【.Net实用方法总结】 整理并总结System.IO中Stream类及其方法介绍
本文主要介绍System.IO命名空间的Stream类,介绍其常用的方法和示例说明。