不在 TPL Task 对象上调用 Dispose() 是不是可以接受?

Posted

技术标签:

【中文标题】不在 TPL Task 对象上调用 Dispose() 是不是可以接受?【英文标题】:Is it considered acceptable to not call Dispose() on a TPL Task object?不在 TPL Task 对象上调用 Dispose() 是否可以接受? 【发布时间】:2011-04-13 15:40:36 【问题描述】:

我想触发一个任务在后台线程上运行。我不想等待任务完成。

在 .net 3.5 中我会这样做:

ThreadPool.QueueUserWorkItem(d =>  DoSomething(); );

在 .net 4 中,建议使用 TPL。我见过推荐的常见模式是:

Task.Factory.StartNew(() =>  DoSomething(); );

但是,StartNew() 方法返回一个实现IDisposableTask 对象。这 似乎被推荐这种模式的人忽略了。 Task.Dispose() 方法的 MSDN 文档说:

“在释放对任务的最后引用之前,始终调用 Dispose。”

在任务完成之前,您不能对任务调用 dispose,因此让主线程等待并调用 dispose 会首先破坏在后台线程上执行的点。似乎也没有任何已完成/已完成的事件可用于清理。

Task 类的 MSDN 页面对此没有评论,《Pro C#2010...》一书推荐了相同的模式,对任务处置不做评论。

我知道如果我离开它,终结器最终会捕获它,但是当我执行大量这样的 fire & forget 任务并且终结器线程不堪重负时,这会回来咬我吗?

所以我的问题是:

在这种情况下不调用Dispose()Task 类上是否可以接受?如果是这样,为什么以及是否存在风险/后果? 是否有任何文档对此进行了讨论? 或者是否有适当的方法来处理我错过的Task 对象? 或者是否有其他方法可以使用 TPL 执行“即发即弃”任务?

【问题讨论】:

相关:The correct way to Fire-and-Forget(见answer) 【参考方案1】:

有一个关于这个in the MSDN forums的讨论。

Microsoft pfx 团队的成员 Stephen Toub 这么说:

Task.Dispose 因 Task 而存在 可能包装事件句柄 等待任务时使用 完成,如果等待 线程实际上必须阻塞(如 反对旋转或可能 执行它正在等待的任务)。 如果你所做的只是使用 延续,该事件句柄将 永远不会被分配 ... 依靠最终确定来处理事情可能会更好。

更新(2012 年 10 月) Stephen Toub 发布了一篇名为 Do I need to dispose of Tasks? 的博客,其中提供了更多详细信息,并解释了 .Net 4.5 中的改进。

总而言之:您不需要在 99% 的时间处理 Task 对象。

释放对象有两个主要原因:及时、确定地释放非托管资源,以及避免运行对象的终结器的成本。大多数情况下,这些都不适用于Task

    从 .Net 4.5 开始,Task 分配内部等待句柄(Task 对象中唯一的非托管资源)的唯一时间是当您显式使用 TaskIAsyncResult.AsyncWaitHandle 时,并且 Task 对象本身没有终结器;句柄本身被包装在一个带有终结器的对象中,因此除非它被分配,否则没有终结器可以运行。

【讨论】:

谢谢,很有趣。虽然它违反了 MSDN 文档。 MS 或 .net 团队是否有任何官方消息表明这是可接受的代码。讨论结束时还提出了一个观点,即“如果未来版本的实现发生变化怎么办” 实际上,我刚刚注意到该线程中的回答者确实在微软工作,似乎在 pfx 团队中,所以我想这是一个官方的答案。但是有建议在它的底部并不适用于所有情况。如果有潜在的泄漏,我最好还是恢复到我知道是安全的 ThreadPool.QueueUserWorkItem 吗? 是的,很奇怪有一个你可能不会调用的 Dispose。如果您在msdn.microsoft.com/en-us/library/dd537610.aspx 和msdn.microsoft.com/en-us/library/dd537609.aspx 此处查看示例,它们不会处理任务。然而,MSDN 中的代码示例有时会展示非常糟糕的技术。回答这个问题的人也为微软工作。 @Simon: (1) 您引用的 MSDN 文档是通用建议,具体情况有更具体的建议(例如,在使用 BeginInvoke 运行代码时不需要在 WinForms 中使用 EndInvoke UI 线程)。 (2) Stephen Toub 作为有效使用 PFX 的常规演讲者而广为人知(例如在 channel9.msdn.com 上),所以如果有人能提供良好的指导,那就是他了。请注意他的第二段:有时将事情留给终结者会更好。【参考方案2】:

这与 Thread 类的问题相同。它消耗 5 个操作系统句柄,但不实现 IDisposable。原设计者的好决定,当然很少有合理的方法来调用 Dispose() 方法。你必须先调用 Join()。

Task 类为此添加了一个句柄,即内部手动重置事件。哪个是最便宜的操作系统资源。当然,它的 Dispose() 方法只能释放那一个事件句柄,而不是 Thread 消耗的 5 个句柄。 Yeah, don't bother.

请注意,您应该对任务的 IsFaulted 属性感兴趣。这是一个相当丑陋的话题,您可以在MSDN Library article 中阅读更多相关信息。一旦你正确处理了这个问题,你的代码中也应该有一个很好的位置来处理任务。

【讨论】:

但是大多数情况下任务不会创建Thread,它使用线程池。【参考方案3】:

我很乐意看到有人对这篇文章中展示的技术进行权衡:Typesafe fire-and-forget asynchronous delegate invocation in C#

看起来像一个简单的扩展方法将处理与任务交互的所有琐碎情况,并能够对其调用 dispose。

public static void FireAndForget<T>(this Action<T> act,T arg1)

    var tsk = Task.Factory.StartNew( ()=> act(arg1),
                                     TaskCreationOptions.LongRunning);
    tsk.ContinueWith(cnt => cnt.Dispose());

【讨论】:

当然,无法处理 ContinueWith 返回的 Task 实例,但请参阅 Stephen Toub 的引用是公认的答案:如果没有执行阻塞等待,则没有可处理的内容一项任务。 正如 Richard 提到的,ContinueWith(...) 还返回第二个 Task 对象,然后该对象不会被释放。 因此,ContinueWith 代码实际上比 redudant 更糟糕,因为它会导致创建另一个任务来处理旧任务。以这种方式,基本上不可能在这个代码块中引入阻塞等待,除非你传递给它的动作委托试图操纵任务本身也是正确的? 您可以使用 lambdas 捕获变量的方式来处理第二个任务。 Task disper = null; disper = tsk.ContinueWith(cnt =&gt; cnt.Dispose(); disper.Dispose(); ); @GideonEngelberth 似乎必须工作。由于 disper 永远不应该被 GC 处理掉,它应该保持有效,直到 lambda 调用自己来处理,假设引用在那里仍然有效/耸耸肩。也许需要一个空的尝试/捕捉它?

以上是关于不在 TPL Task 对象上调用 Dispose() 是不是可以接受?的主要内容,如果未能解决你的问题,请参考以下文章

当传递到另一个对象时,谁应该在 IDisposable 对象上调用 Dispose?

您不应在一个对象上多次调用 Dispose(CA2202)[重复]

代码分析警告 CA2000:在对象“new ContainerControlledLifetimeManager()”上调用 Dispose

使用TPL取回Task中的运行结果的三种方式

我应该为流对象调用 Close() 或 Dispose() 吗?

TPL异步并行编程之回调