有许多人问我这个问题:
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做了一些更改:
-
我们大大降低了Task的WaitHandle被分配的可能性。我们重新实现了WaitAll和WaitAny,从而使它们不再依赖于Task的WaitHandle。而且我们避免在任何新的Task或者.Net 4.5 引入的异步/等待功能内部使用它。因此,只有在你显式地使用了 Task的IAsyncResult.AsyncWaitHandle时WaitHandle会被分配,但这十份罕见。这意味着除非在非常少见的情况下,否则对Task执行dispose完全没必要。
-
我们使Task即使在被 dispose后仍然可用。现在即使在task执行dispose了,它的所有公共成员依然可以被使用,就像没有被dispose之前一样,唯一不能被使用的是IAsyncResult.AsyncWaitHandle,因为在你dispose了task实例的时候它才会被真正的dispose。如果你在Task被Dispose了之后使用IAsyncResult.AsyncWaitHandle这个属性,会抛出ObjectDisposedException异常。这意味着你在对成功完成执行的Task进行缓存时感到心里面畅快,知道它们很干净。另外,更进一步地说,IAsyncResult现在的使用频率应该显著下降了,我们有async/await和基于Task的Async模式,即使继续使用IAsyncResult,使用AsyncWaitHandle就更加罕见了。
-
对于新的.NET for Metro style apps引用组件和表面区域 ,Task甚至没有实现IDispose。所以对于Metro风格的app,或者对于跨这些app的轻型库,你甚至没有使用task的dispose的机会,这是个好事情。
建议
所以,让我们回到简短的答案:“不,不用去烦恼dispose你的task”。通常没有合适的场合可以这么做,也几乎没有什么原因去这么做,根据你使用的组件、程序集,你或许都没这么去做的机会。