002-我需要Dispose Task吗

有许多人问我这个问题:

Task实现了IDispose接口,而且提供了Dispose方法。这意味着我应该Dispose所有的Task吗?

摘要

这是我简短的回答:不,不用非要Dispose你的Task。

这是我中等长度的答案:

不用。不用费心去Dispose你的Task,除非性能或者弹性测试需要你去基于使用方式去Dispose Task来达到性能目标。当你需要去Dispose 那些Task,仅在情形简单的时候去做,即当你100%确认代码中Task已经成功完成(IsCompleted为true)而且没有其他人使用这些Task。

如果有喝咖啡并阅读的时间,可以看看下面的长答案:

 

为什么有Task.Dispose?

在高的设计层面,.Net Framework设计准备表明如果一个类型持有其他的IDispose资源,那么它应该实现IDispose接口。所以Task有Dispose方法。在内部,Task可以分配一个被用来等待Task成功完成的WaitHandle。WaitHandle实现了IDispose接口,因为它内部持有实现了IDispose的SafeWaitHandle。SafeWaitHandle包含一个本地Handle资源:如果SafeWaitHandle没有Dispose,最终它的终结器(finalizer)会清理所有的被包含的handle资源,但是与此同时它的资源不会被清理干净,这会给系统造成压力。通过Task实现IDispose接口,我们使得关心积极清理这些资源的开发者可以及时地清理这些资源。

 

带来的问题

如果每一个Task分配一个WaitHandle,出于性能考虑,积极地dispose这些task是个好主意。但事实并非如此。事实上很少的task真正地分配了WaitHandle。在.Net 4里面,WaitHandle在几种情况下被延迟加载:

  • 如果Task.IAsyncResult.AsyncWaitHandle属性(显式实现了接口)被访问

  • 如果Task被Task.WaitAll或者Task.WaitAny调用,且Task尚未成功完成。

而且在.Net 4中,一旦Task被dispose了,它的大多数成员在被访问的时候会抛出ObjectDisposedExceptions异常。这使得缓存已经完成的task变得困难(可能因为性能原因缓存task),因为一旦一个消费者dispose了某个task,另一个消费者将不能访问这个task的重要成员,比如ContinueWith或者这个task的Result。

还有一个复杂的问题,那就是Task是异步的, 如果Task被用于并行,比如在fork/join模式,通常很容易就知道Task何时完成了,什么时候没有其他人使用了。例如

var tasks = new Task[3]; 
tasks[0] = Compute1Async(); 
tasks[1] = Compute2Async(); 
tasks[2] = Compute3Async(); 
Task.WaitAll(tasks); 
foreach(var task in tasks) task.Dispose();

 

但是,当使用Task对异步操作进行排序的时候,经常会更加困难 。比如:

Compute1Async().ContinueWith(t1 => 
{ 
    t1.Dispose(); 
    … 
});

 

这个例子成功的dispose了从Compute1Async返回的Task,但是却没有dispose从ContinueWith返回的Task,这下你就明白了。即使使用c#和Visual Basic中的新关键字 async/await,还是很烂。思考一个操作的基本顺序,例如:

string s1 = await Compute1Async(); 
string s2 = await Compute2Async(s1); 
string s3 = await Compute3Async(s2);

 

如果我想Dispose这些Task,我需要像下面这样重写:

string s1 = null, s2 = null, s3 = null; 
using(var t1 = Compute1Async()) 
    s1 = await t1; 
using(var t2 = Compute2Async(s1)) 
    s2 = await t2; 
using(var t3 = Compute3Async(s2)) 
    s3 = await t3;

尴尬。

 

解决方法

考虑到大多数task没必要dispose,而且这样做很尴尬,所以在.Net 4.5中我们给Task.Dispose做了一些更改:

  1. 我们大大降低了Task的WaitHandle被分配的可能性。我们重新实现了WaitAll和WaitAny,从而使它们不再依赖于Task的WaitHandle。而且我们避免在任何新的Task或者.Net 4.5 引入的异步/等待功能内部使用它。因此,只有在你显式地使用了 Task的IAsyncResult.AsyncWaitHandle时WaitHandle会被分配,但这十份罕见。这意味着除非在非常少见的情况下,否则对Task执行dispose完全没必要。

  2. 我们使Task即使在被 dispose后仍然可用。现在即使在task执行dispose了,它的所有公共成员依然可以被使用,就像没有被dispose之前一样,唯一不能被使用的是IAsyncResult.AsyncWaitHandle,因为在你dispose了task实例的时候它才会被真正的dispose。如果你在Task被Dispose了之后使用IAsyncResult.AsyncWaitHandle这个属性,会抛出ObjectDisposedException异常。这意味着你在对成功完成执行的Task进行缓存时感到心里面畅快,知道它们很干净。另外,更进一步地说,IAsyncResult现在的使用频率应该显著下降了,我们有async/await和基于Task的Async模式,即使继续使用IAsyncResult,使用AsyncWaitHandle就更加罕见了。

  3. 对于新的.NET for Metro style apps引用组件和表面区域 ,Task甚至没有实现IDispose。所以对于Metro风格的app,或者对于跨这些app的轻型库,你甚至没有使用task的dispose的机会,这是个好事情。

 

建议

所以,让我们回到简短的答案:“不,不用去烦恼dispose你的task”。通常没有合适的场合可以这么做,也几乎没有什么原因去这么做,根据你使用的组件、程序集,你或许都没这么去做的机会。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值