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


123

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

在.net 3.5中,我应该这样做:

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

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

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

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

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

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

Task类的MSDN页面没有对此发表评论,而《 Pro C#2010 ...》这本书推荐了相同的模式,并且对任务处理没有评论。

我知道如果我只留下它,终结器最终会抓住它,但是当我做很多工作而忘了像这样的任务而终结器线程不堪重负时,这会回来咬我吗?

所以我的问题是:

  • 是否可以接受不叫Dispose()Task在这种情况下类?如果是这样,为什么而且存在风险/后果?
  • 有没有讨论这个的文档?
  • 还是有适当的方法来处理Task我错过的对象?
  • 还是有另一种通过TPL做事忘了任务的方法?

Answers:


108

MSDN论坛中对此进行了讨论

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

Task.Dispose之所以存在,是因为Task可能包装了在等待任务完成时使用的事件句柄,以防等待线程实际不得不阻塞(而不是旋转或潜在地执行其正在等待的任务)。如果您所做的只是使用延续,则将永远不会分配该事件句柄
……
最好依靠终结来处理事情。

更新(2012年10月)
Stephen Toub发布了一个博客,标题为“我需要处理任务吗?其中提供了更多详细信息,并说明了.Net 4.5中的改进。

总结:不需要Task99%的时间处理对象。

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

  1. 从.Net 4.5开始,a唯一Task分配内部等待句柄(Task对象中唯一的非托管资源)的时间是当您显式使用的IAsyncResult.AsyncWaitHandleTask,并且
  2. Task对象本身没有终结; 该句柄本身使用终结器包装在一个对象中,因此,除非分配了该句柄,否则就无法运行终结器。

3
谢谢,有趣。但是,它违反了MSDN文档。MS或.net团队有没有官方的说法,那就是可接受的代码。在讨论的最后,还有一个观点提出:“如果实现在将来的版本中发生变化,将会怎样?”
Simon P Stevens

实际上,我刚刚注意到,该线程中的应答程序确实在microsoft上确实起作用,似乎在pfx团队中也是如此,因此我认为这是一种正式的答案。但是,有人建议它的底部在所有情况下都不起作用。如果存在潜在的泄漏,我最好只恢复到我知道是安全的ThreadPool.QueueUserWorkItem吗?
西蒙·史蒂文斯

是的,有一个您可能不会调用的Dispose,这很奇怪。如果您在msdn.microsoft.com/zh-cn/library/dd537610.aspxmsdn.microsoft.com/en-us/library/dd537609.aspx处查看示例,则它们不会处理任务。但是,MSDN中的代码示例有时会展示非常糟糕的技术。在这个问题上回答的那个人也适用于Microsoft。
Kirill Muzykov

2
@Simon:(1)您引用的MSDN文档是一般性建议,具体情况有更具体的建议(例如,在UI线程上运行代码EndInvoke时,无需在WinForms中使用BeginInvoke)。(2)Stephen Toub作为有效使用PFX的定期发言人而广为人知(例如,在channel9.msdn.com上),因此,如果有人可以提供良好的指导,那就行了。请注意他的第二段:有时将事情留给终结器更好。
理查德(Richard)2010年

12

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

Task类为此添加了一个句柄,这是一个内部手动重置事件。这是最便宜的操作系统资源。当然,其Dispose()方法只能释放一个事件句柄,而不释放Thread占用的5个句柄。 是的,不要打扰

请注意,您应该对该任务的IsFaulted属性感兴趣。这是一个相当丑陋的话题,您可以在此阅读更多内容 MSDN Library文章中。一旦正确处理了这些问题,您就应该在代码中有一个不错的位置来处理任务。


6
但是任务Thread在大多数情况下不会创建,而是使用ThreadPool。
2011年

-1

我很乐意看到有人对本文中显示的技术有所重视:C#中的Typesafe即弃即忘异步委托调用

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

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());
}

3
当然,这无法处理由Task返回的实例ContinueWith,但是可以看到斯蒂芬·图布(Stephen Toub)的报价是公认的答案:如果没有任何事情对任务执行阻塞等待,则没有任何东西可以处置。
理查德2010年

1
正如Richard所提到的,ContinueWith(...)还返回了第二个Task对象,然后该对象不再被处置。
西蒙·史蒂文斯

1
这样看来,ContinueWith代码实际上比冗余代码差,因为它将导致创建另一个任务以处理旧任务。按照这种方式,除了您传递给它的动作委托试图操纵Tasks本身是否正确之外,基本上不可能在此代码块中引入阻塞等待。
克里斯·马里西奇

1
您可能可以使用lambda如何以一些棘手的方式捕获变量来完成第二项任务。Task disper = null; disper = tsk.ContinueWith(cnt => { cnt.Dispose(); disper.Dispose(); });
Gideon Engelberth

@GideonEngelberth似乎必须工作。由于分散永远不会被GC丢弃,因此在lambda调用自身进行处置之前,它应该保持有效,并假设该引用在那里仍然有效。也许需要一个空的try / catch?
克里斯·马里西奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.