【问题标题】:Is it harmless to call GC.SuppressFinalize within the finalizer?在终结器中调用 GC.SuppressFinalize 是否无害?
【发布时间】:2019-09-21 22:48:55
【问题描述】:

因为finalizer/IDisposable 和所谓的“IDisposable 模式”主题往往会引发大量装腔作势、夸夸其谈和好战的观点(不是——分别是here、@987654322 @、here 等等),我真的很犹豫要问这个问题。希望先发制人那些陈旧的辩论,我坚持一个非常简单的问题,在 StackOverflow 上似乎没有简明的答案......

一旦对象的终结器开始执行,调用GC.SuppressFinalize(this) 是否是空的?更具体或更有用(当然),从终结器本身调用GC.SuppressFinalize(this) 是否无害? (同样,我们在这里不讨论任何“为什么”)

换句话说,除了调用 API 的开销并在对象标头中适当地设置一个标志之外,是否存在任何不良、不需要或其他明显的正确性或性能影响?

【问题讨论】:

  • Is calling GC.SuppressFinalize(this) vacuous once the object's finalizer has begun executing? - 根据Remarks section 中的措辞,是的。尽管如此,according to the IDisposable pattern,这不是放置GC.SuppressFinalize 的地方。
  • @GSerg 谢谢,你说得对,is 是文档摘录中的 wording 问题,我我想我希望能更明确地确定它。至于那个所谓的“模式”。我试图阻止这种诱饵......
  • 如果它在文档中,那么这就是明确的答案。如果您在执行文档中所说的操作时遇到问题,那么 Microsoft 会将其视为错误(他们可能不会修复它,但 GC 和终结是重要的事情)。处置模式一直以来都有很好的记录 - 我很惊讶有争议。也就是说,你确定你需要一个终结器吗?你读过终结者不能做的所有事情吗?编写终结器的需求很少见,而编写一个可靠的终结器很难。
  • 它只是对象中的一个标志,如果用于其预期目的将没有明显的开销(实际上它是 GC 的排序优化(松散使用的术语)并且具有非常特定的目的),您是否需要标记不需要最终确定或所有非托管代码已被清理?在 finalizer 开始后调用SuppressFinalize 似乎很可疑,这不是它的预期用例,也与其生活目标背道而驰。正如我们所知(我相信你也知道)终结器/析构函数是一个非常热门的话题,因为它们几乎没有用例
  • @Flydog57 问题是文档并没有完全准确地回答这个问题。它说,“如果 obj 没有终结器,则对 SuppressFinalize 方法的调用无效。”这并不一定意味着SuppressFinalize 具有终结器的对象没有影响。

标签: c# garbage-collection finalizer


【解决方案1】:

当然,最好完全避免使用终结器,并使用SafeHandle,正如现代成语所要求的那样。然后所有关于终结器的东西都变得毫无意义。

也就是说,尽管这样做很明智,但从终结器调用 GC.SuppressFinalize() 是完全安全的。 The documentation for the method 描述了该方法的作用:

此方法在 obj 的对象头中设置一个位,运行时在调用终结器时会检查该位。

运行时实际上也可能在 GC 操作期间检查此位,即当发现对象无法访问时,该对象的终结器将被放入终结器队列。如果它在那个时候设置,终结器甚至不会在队列中结束。

稍后再次检查它,在调用终结器本身之前,也可以避免对象的终结,如果结果证明某个其他对象的终结器最终处置它,即使该对象的终结器已放入终结队列。

这两项检查都在调用终结器之前进行。一旦调用了终结器,对象中的位就没有用途了。设置它是无害的,但不会完成任何事情。

顺便说一句:请注意,过去的 .NET 实现使用 FinalizerFReachable 队列。当一个对象被创建时,如果它有一个终结器,它将被移动到Finalizer 队列中。一旦对象无法访问,它将被移动到FReachable 队列以供稍后完成。调用SuppressFinalize() 将从Finalizer 队列中删除对象。到终结器运行时,对象不再在此队列中,因此 SuppressFinalize() 调用将是 NOP,同样无害。

也就是说,您的问题很宽泛:“……是否存在任何不好的、不想要的或其他明显的正确性或性能影响?”。其中大部分是在旁观者的眼中。我认为调用GC.SuppressFinalize() 的终结器是不正确的。所以,这对我来说是一个“有形的正确性效应”。我还发现偏离已发布的、公认的标准模式的代码是“不需要的”。如果问题中没有更具体的标准来限制它,那么问题部分的答案可能是“是”、“否”、“有时”等。

实际上,您的问题有一个重复的问题,但没有人愿意回答:Calling GC.SuppressFinalize() from within a finalizer。不过,我确实找到了 cmets 的线索,尤其是 Eric Lippert 的贡献:

您的假设是对 SuppressFinalize 的不必要调用是您计划中的错误。那不是问题。问题是在终结器线程上处理 托管 资源。回想一下终结器在它们自己的线程上运行,并且托管资源可以是线程关联的,现在开始想象可能导致的恐怖。此外:终结器以任意顺序运行。在终结器线程上部署的托管对象可能已经终结;现在您可能在一个对象上运行了两次终结逻辑;它对那种情况很健壮吗? ——埃里克·利珀特 2016 年 3 月 31 日 21:58 1

编写一个正确的终结器是非常困难的,我建议你不要尝试,理想情况下,但一定要等到你更好地理解这个模式。如果你还不够害怕,我关于这个主题的系列文章可能会让你更加害怕:ericlippert.com/2015/05/18/… – Eric Lippert 2016 年 3 月 31 日 21:59

@Tom:问题是“我完全错误地使用了 dispose 模式;这是我做错了什么的特定部分吗?” 不,从第一句话开始,整个事情就错了。您不使用 Dispose 来释放托管资源,当然也不会为此使用终结器。这就是这里的问题。从终结器调用 SuppressFinalize 本身有什么问题吗?好吧,它会起作用,但是不应该是正确的做法,所以它是否起作用应该是无关紧要的。 – Eric Lippert 7 月 7 日 14:17

@Tom:另外,你为什么首先调用 SuppressFinalize?只是因为它是一种性能优化。但是在什么情况下,从终结器线程调用时是一种优化?只有当你未能从主线程进行优化时!那就是进行优化的地方! – 埃里克·利珀特 7 月 7 日 14:24

恕我直言,这些 cmets 将主要问题带到了一个很好的点:询问从终结器调用 SuppressFinalize() 是否安全是错误的问题。如果你已经到了不得不问这个问题的地步,那么代码已经错了,问题的答案可能并不那么相关。正确的方法是修复代码,这样您就不必问这个问题了。

最后,虽然不是完全相同的问题,但我认为还值得指出的是,在Dispose() 方法末尾调用SuppressFinalize() 的通常指导可能是不正确的。如果被调用,它应该在Dispose() 方法的开始处被调用。见Be Careful Where You Put GC.SuppressFinalize

【讨论】:

  • 不错的答案,而且非常冗长,加一个
  • 我同意,写得很好。正如hinted 在对该问题的评论中一样,唯一可能的狡辩是文体/哲学,即您明确表示且完全有权遵守标准模式的偏好,可能排除了最大化 DRY 代码共享。但再说一次,让我们都同意不去那里......所以是的,我们都很好。
【解决方案2】:

简而言之,很少使用Finalizer,您应该确定性地清理您的应用程序。此外,还有竞争条件是 .net 中的 finalization 存在问题的其他原因{在此处插入大量博客文章和一长串建议}。

当你确实需要一个终结器时,除了Dispose,你还需要它,而不是Dispose

在大多数用例中,如果您要使用 finalizer,您会在 Dispose 方法中调用 SuppressFinalize,但要回答问题(以及其他问题) )

一旦对象的调用 GC.SuppressFinalize(this) vacuous 终结器已经开始执行了吗?

SuppressFinalize 的调用实际上很简单,它只是在对象中设置一个标志以防止它被添加到终结队列中。它本身几乎没有开销,并且可以在任何代码路径中设置。这可能不是您的问题。

然而,这里真正的问题是阻止 GC finalizing 和你的对象 disposing 同时,以及关于状态的假设在执行期间您周围的系统。

要尝试缓解这种情况,您可以尽快致电GC.SuppressFinalize(this),使用标志来确定您是否已处置。甚至检查 appdomainenvironment 是否已开始卸载或关闭

if (!Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload())

无论哪种方式,这里仍然存在竞争条件和问题,缓解或处理它们将取决于您需要 finalizer 的确切原因以及您要清理的内容以及如何清理。 p>

【讨论】:

  • 感谢您的回答。我同意涉足任何潜在的 GC 竞争是不明智的,因此我使用的更好策略是始终在每个对象的 own 上部署(无锁)原子保证代码b> 数据字段,通常两个(或其中一个)Dispose() 或终结器方法,视情况而定。无论何时或由谁或多少次或可重入地调用任何一种方法,它都尽可能地防弹,实际上并不难做到。是的,终结器可能永远不会被调用,所以它只能被视为“好公民”的清理尝试。
  • "当您确实需要一个终结器时,您希望它另外 用于 Dispose,而不是 Dispose。"伟大的规则,永远正确。 +1
猜你喜欢
  • 1970-01-01
  • 2010-10-11
  • 1970-01-01
  • 2010-12-07
  • 2021-07-27
  • 1970-01-01
  • 1970-01-01
  • 2014-12-07
  • 1970-01-01
相关资源
最近更新 更多