Delegate.EndInvoke()是否真的必要?

25

我曾经阅读一些论坛和一两个stackoverflow问题,说使用Delegate.BeginInvoke时必须使用Delegate.EndInvoke。 然而,许多关于使用BeginInvoke的文章都没有提到使用EndInvoke。 此外,我已经部署了仅使用BeginInvoke的生产代码,并且似乎没有任何内存问题。 我通常使用BeginInvoke处理那些我不在意它们何时完成或处理需要多长时间的线程。


2
您将会“泄漏”内存10分钟,这是默认的远程生存期超时。是的,通常不容易察觉。使用ThreadPool.QUWI来处理fire-and-forget代码。 - Hans Passant
但是我听到的所有信息都说BeginInvoke使用线程池... - Achilles
2
仅仅因为它使用线程池并不意味着它与调用QUWI完全相同。你可以设计一个类,异步地触发一个方法,然后将一个2GB的文件保存到硬盘上;这将“使用线程池”,但不同于仅仅调用QUWI方法(而不保存文件)。 - Dan Tao
7个回答

45

根据MSDN文章『将同步方法异步调用』

无论使用哪种技术,都要调用EndInvoke来完成异步调用。

现在,有理论也有实践。像你之前的许多其他开发人员一样,你可能常常忽略文档要求,但实际上可以运行程序。不过问题在于:EndInvoke是否真正执行了防止应用程序崩溃、泄漏内存等必备操作,这可能是一个实现细节。但是有个事实需要注意,如果它是文档要求,那么你确实应该按照要求去做。这不仅是理论,也是为了在发生变化时保护自己。

通过记录下这个要求,异步调用机制的设计者基本上给自己留下了改变BeginInvoke和EndInvoke工作方式的自由,因此,如果有足够的原因(例如性能增强),EndInvoke可能会突然变得更加必要。 假设如果您忘记调用EndInvoke,它将突然导致死锁。他们已经通过说『总是调用EndInvoke』来覆盖自己;如果您的应用程序因为没有遵循此要求而停止工作,则责任在于您。

我并不是说这一定是一个可能发生的情况。我的观点只是你不应该或至少我不会以『如果可以省略,那就不必要』的心态去问:『这真的有必要吗?』,因为文档确实要求你这样做。


1
这个答案绝对值得点赞。 - Puppy
这对我来说很困难。正如你所指出的,在实践中它并不是一个要求,但为了使应用程序能够容忍框架的变化,应该使用它。 - Achilles

14

如果您计划从线程中抛出异常并且希望正确地捕获它们,那么调用EndInvoke是绝对必要的。如果您不调用EndInvoke,则抛出异常的线程将消失,并且您将无法得知任何信息。

为了支持EndInvoke,请提供一个AsyncCallback,然后在该回调方法中,请确保使用try/catch块包装对EndInvoke的调用。

虽然如果您不关心线程中发生的事情,可以不调用EndInvoke,但我认为只是调用EndInvoke是一个非常好的习惯。您永远不知道,某天初级开发人员可能会进入代码并抛出异常。然后更新的应用程序被部署,并开始调用服务。


投了一票。完全忘记了异常吞噬问题。很好的提醒!我通常直接将所有内容放入线程池,但异常捕获改变了一切。 - Gusdor

9

我听说可能会出现内存泄漏问题。

通过关键字搜索,我找到了一个很好的讨论。

不调用EndInvoke *真的*会导致内存泄漏吗?

可能会,但不一定。在.NET中,技术上不存在内存泄漏。最终,内存将被GC回收。问题是它可能存在很长时间。您应该调用EndInvoke的原因是因为调用的结果(即使没有返回值)必须由.NET缓存,直到调用EndInvoke。例如,如果调用的代码引发异常,则异常会缓存在调用数据中。在调用EndInvoke之前,它仍然存在于内存中。调用EndInvoke后,内存可以被释放。

这是参考资料

另一个参考资料


在.NET中使用GCHandle实际上很容易创建内存泄漏。此外,问题通常不是内存泄漏,而是资源泄漏。如果某种逻辑打开操作(例如BeginInvoke)告诉您应始终调用其逻辑关闭操作(例如EndInvoke),那么我认为您应该这样做。 - Stephen Cleary
1
开放式内存泄漏,至少按我的定义,可以在托管内存中轻松创建,而无需GCHandles、COM互操作或任何类似的东西。开放式内存泄漏将导致内存被分配在完全无用的方式下,它不能被垃圾回收,直到发生某个特定事件,并且在此之前可能会分配无限量的内存。那些将被完整GC清理的东西不是任何合理定义下的内存泄漏,但不会被清理的东西可能是内存泄漏。 - supercat

1

MSDN指出,调用EndInvoke非常重要:

重要提示 无论使用哪种技术,都必须调用EndInvoke来完成异步调用。


1

已经有记录表明,在WinForms应用程序中使用BeginInvoke在GUI线程上执行操作时,不需要EndInvoke(假设您不等待IAsyncResult)。

然而,这是一个特定的例外,适用于一般规则:对于每个BeginOperation,必须有一个匹配的EndOperation。如另一个A所述:如果GUI访问代码可能会抛出异常,则需要EndInvoke来获取异常。

请参见此处(有点)官方确认:http://blogs.msdn.com/b/cbrumme/archive/2003/05/06/51385.aspx#51395(这是Chris Brummie的评论2003年5月12日下午5:50)。

另外:关于Control.BeginInvoke的文档中包含了以下备注:

如果需要,您可以调用EndInvoke来检索委托的返回值,但这不是必需的。EndInvoke将会阻塞,直到返回值可以被检索。

所以,官方声明:使用异步委托在GUI线程上执行操作时,WinForm不要求使用EndInvoke(除非您需要返回值或可能出现异常,在任何情况下都考虑使用Invoke)。


1
ControlBeginInvoke/EndInvokeDelegateBeginInvoke/EndInvoke 完全不同。你不需要调用 Control.EndInvoke,但这与异步委托无关。在 WinForms 应用程序中,你应该为每个异步委托调用 Delegate.EndInvoke - Stephen Cleary
1
@StephenCleary:确实不同 - 就我所见,Control.BeginInvoke是唯一的例外。 - Richard

1

我通常使用BeginInvoke的方式是处理那些我不关心何时完成或需要多长时间来处理的线程。

听起来你应该考虑使用Thread类而不是异步委托。

如果你关心结果或检测错误(这两者都需要将结果/错误转发到原始线程),那么你可以使用异步委托,在这种情况下,你需要EndInvoke。更好的方法是使用Task或Task<TResult>类。

如果你只想启动一些独立的操作,那么请使用Thread类。


1

来自Windows Form文档中的Control.BeginInvoke()

如果需要,您可以调用EndInvoke从委托中检索返回值,但这不是必需的。EndInvoke将阻塞,直到可以检索返回值。

这是关于UI线程上的Windows Form异步调用的特殊情况,这不适用于一般情况,但对于处于此情况的人可能会有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接