为什么CLR需要一个专用线程来调用finalize方法?

8
这是一篇来自 MSDN 文章的节选。可以在这里找到完整文章。
有一个专门的运行时线程用于调用 Finalize 方法。当 freachable 队列为空时(通常情况下),该线程会休眠。但是当条目出现时,该线程会唤醒,从队列中删除每个条目,并调用每个对象的 Finalize 方法。因此,在 Finalize 方法中不应执行任何对执行代码的线程做出任何假设的代码。例如,在 Finalize 方法中避免访问线程本地存储。
我以前问过这个问题,并因为同样的原因被投票否决说没有专门的线程用于可终结对象,我的信息来源是错误的。如果这篇文章所解释的是真的,那么我想知道 CLR 为什么需要一个专门的线程来调用 Finalize 方法?

2
这可能是负责垃圾回收的同一线程吗?语言规范中有关于此的信息吗?如果是在同一个线程中运行GC,那么你的答案就是在GC执行代管理、检查未根对象等时防止其他代码的执行。 - Samuel
你应该非常小心依赖于2000年的文章,自那时以来,在.Net处理事务的方式和 .Net在4.5中进行垃圾回收的方式都发生了很多变化。 - AK_
1
我不确定“需要”是否是正确的术语。作为一项实现的细节,实现可以选择使用专用线程执行最终操作,但我认为这不是符合要求的实现的必要条件。 - Damien_The_Unbeliever
另一种方法是选择执行GC的线程并让它执行finalizers。在此过程中,程序必须保持冻结状态。你更喜欢哪种方法? - Hans Passant
1
我想指出一些终结器可能需要很长时间 - 特别是半关闭(或者是半打开?)的套接字。你想让你的应用程序因为你忘记显式处理某个套接字而停止四分钟吗?但基本点真正的是,处理终结器最简单的方法是在一个单独的线程中 - 你的终结器代码应该完全与应用程序隔离,这使得它非常线程安全。你需要一个理由使用线程,而不是相反。 - Luaan
3个回答

3
没有什么关于终结器需要单独线程的,但考虑到以下情况:当GC运行在0代和1代上,即“短暂”的代时,托管线程会被挂起;在托管线程恢复后,2代可以进行并发或后台收集(MSDN中有很多关于此的信息:垃圾回收基础)。然而,一个可能的天真解决方案是这样做:挂起所有线程(或可能让触发GC的线程继续运行,只需让它执行GC);进行垃圾回收,包括所有相关内容;恢复所有线程。这可能很容易实现。GC操作可以自由访问与堆相关的所有数据结构,并且可以自由地进行任何操作,因为没有访问这些东西的其他线程正在运行。然而,这将使程序不时出现“相当长”的暂停,在大多数应用中都是无法接受的。
因此,负责GC的团队选择了尽可能短暂地暂停,仅在需要时(第0和1代收集)暂停,同时在收集第2代时保持应用程序运行。这更加困难。请注意,这是我个人的假设,我没有访问实际设计文档或任何关于此的可靠信息。这些假设基于官方文档。
所以回到终结器线程。这些对象已经“死亡”并准备进行回收,所以为什么要让主要执行应用程序工作的线程处理这个问题呢?由于GC已经在后台执行操作,以同样的方式处理死亡对象的终结化似乎是一个好决策。

0
  1. 终结器是您的最后一道防线 - 如果您的资源没有得到正确处理,那么在将来的某个时刻,CLR本身将尝试执行清理。这个过程是完全不确定的 - 它不是RAII。它不适用于处理某些事件处理程序取消挂接或类似的情况 - 它应该清理您的实例使用并且没有正确处理(unmanaged)资源 (即使没有直接终结器,管理的资源也会被处理,至少如果没有任何循环依赖关系)。因此,在您的工作线程上执行它是没有必要的。
  2. 总体垃圾回收对于您的程序是不确定性和无干扰的 - 当CLR决定执行时,它会发生,而您不知道它的发生时间,并且不能真正影响它(好吧,有GC class,但仍然如此)。
  3. 非托管资源可能非常庞大,应尽快处理。
  4. 即使需要在同一线程上执行终结器,它又该如何实现呢?为了在某个任意时间点上在线程上执行代码,它[线程]必须提供某种同步上下文,而不是每个人都可以提供。因此,每个线程都必须提供这样的同步上下文 - 这不是最好的想法。

考虑到所有这些观察,为最终化专门创建一个线程似乎是相当合理和有效的解决方案。


0

您可以利用并发来提高性能,特别是在具有多个 CPU 核心的现代处理器架构上。垃圾回收非常有用,但它可能会对性能产生一些影响,因为实际上,在垃圾收集器重新排列内存中对象的布局时,您的应用程序将无法执行任何操作。然而,任何终结操作都可以在后台执行,这就是为什么它在单独的线程上执行以提高性能的原因。


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