线程防止所有者的垃圾收集

Posted

技术标签:

【中文标题】线程防止所有者的垃圾收集【英文标题】:Thread preventing garbage collection of owner 【发布时间】:2013-03-19 17:28:25 【问题描述】:

在我创建的库中,我有一个 DataPort 类,它实现类似于 .NET SerialPort 类的功能。它与某些硬件对话,并在数据通过该硬件进入时引发事件。为实现此行为,DataPort 启动一个线程,该线程预期与 DataPort 对象具有相同的生命周期。 问题是当 DataPort 超出范围时,它永远不会被垃圾回收

现在,因为 DataPort 与硬件通信(使用 pInvoke)并拥有一些非托管资源,所以它实现了 IDisposable。当您在对象上调用 Dispose 时,一切都会正确发生。 DataPort 摆脱了它所有的非托管资源并终止了工作线程并消失了。但是,如果您只是让 DataPort 超出范围,那么垃圾收集器将永远不会调用终结器,并且 DataPort 将永远在内存中保持活动状态。我知道发生这种情况有两个原因:

    终结器中的断点永远不会被命中 SOS.dll 告诉我 DataPort 还活着

侧边栏:在我们继续之前,我要说是的,我知道答案是“调用 Dispose() Dummy!”但我认为,即使您让所有引用超出范围,最终应该会发生正确的事情,并且垃圾收集器应该摆脱 DataPort

回到问题:使用 SOS.dll,我可以看到我的 DataPort 没有被垃圾收集的原因是它启动的线程仍然具有对 DataPort 对象的引用- 通过线程正在运行的实例方法的隐式“this”参数。正在运行的工作线程will not be garbage collected,因此在运行工作线程范围内的任何引用也不符合垃圾回收条件。

线程本身基本上运行以下代码:

public void WorkerThreadMethod(object unused)

  ManualResetEvent dataReady = pInvoke_SubcribeToEvent(this.nativeHardwareHandle);
  for(;;)
  
    //Wait here until we have data, or we got a signal to terminate the thread because we're being disposed
    int signalIndex = WaitHandle.WaitAny(new WaitHandle[] this.dataReady, this.closeSignal);
    if(signalIndex == 1) //closeSignal is at index 1
    
      //We got the close signal.  We're being disposed!
      return; //This will stop the thread
    
    else
    
      //Must've been the dataReady signal from the hardware and not the close signal.
      this.ProcessDataFromHardware();
      dataReady.Reset()
    
  

Dispose 方法包含以下(相关)代码:

public void Dispose()

  closeSignal.Set();
  workerThread.Join();

因为线程是 gc 根并且它持有对 DataPort 的引用,所以 DataPort 永远不符合垃圾收集的条件。因为永远不会调用终结器,所以我们永远不会向工作线程发送关闭信号。因为工作线程永远不会收到关闭信号,所以它会一直运行并保持该引用。确认!

对于这个问题,我能想到的唯一答案是去掉 WorkerThread 方法上的“this”参数(在下面的答案中有详细说明)。其他人能想到另一种选择吗?必须有更好的方法来创建具有相同生命周期的对象的线程!或者,这可以在没有单独线程的情况下完成吗?我在 msdn 论坛上选择了这个基于 this post 的特殊设计,该论坛描述了常规 .NET 串行端口类的一些内部实现细节

更新来自 cmets 的一些额外信息:

有问题的线程已将 IsBackground 设置为 true 上面提到的非托管资源不会影响问题。即使示例中的所有内容都使用托管资源,我仍然会看到相同的问题

【问题讨论】:

您应该使用派生自SafeHandleCriticalHandle 的类来包装您的非托管资源。如果您的库中的任何类的终结器没有扩展这两者之一,那么您可能有一个设计缺陷,这是一个等待发生的主要错误。当然也有例外,但它们非常罕见,以至于我已经有一段时间没有遇到过。这是a starting point,用于理解这些东西;如果您需要有关非托管清理的更多参考资料,请随时与我联系。 从这里记忆,但线程不会创建隐式 gc 根吗? (也许除非它们被设置为 isbackground?) @280Z28 这个问题的 P/Invoke/unmanaged 部分可能不相关,但它在示例的第一部分中泄露了出来。唯一涉及的非托管资源是 dll 在 Open() 方法中返回的硬件句柄,我已经将其实现为 SafeHandle。 dataReady ManualResetEvent 被传递到非托管世界,但 P/Invoke 编组器负责处理。如果没有非托管资源,问题仍然会发生。 DataPort 不会被垃圾回收,它拥有的线程将永远存在。 @JerKimball 我相信有问题的线程已经将 IsBackground 设置为 true,因为它不会使进程保持活动状态,但我会仔细检查 【参考方案1】:

为了摆脱隐含的“This”参数,我稍微改变了工作线程方法,并将“this”引用作为参数传递:

public static void WorkerThreadMethod(object thisParameter)

  //Extract the things we need from the parameter passed in (the DataPort)
  //dataReady used to be 'this.dataReady' and closeSignal used to be
  //'this.closeSignal'
  ManualResetEvent dataReady = ((DataPort)thisParameter).dataReady;
  WaitHandle closeSignal = ((DataPort)thisParameter).closeSignal;

  thisParameter = null; //Forget the reference to the DataPort

  for(;;)
  
    //Same as before, but without "this" . . .
  

令人震惊的是,这并没有解决问题!

回到 SOS.dll,我看到仍然有一个对我的 DataPort 的引用被一个 ThreadHelper 对象持有。显然,当您通过执行Thread.Start(this); 启动工作线程时,它会创建一个私有 ThreadHelper 对象,该对象的生命周期与保存您传递给 Start 方法的引用的线程相同(我在推断)。这给我们留下了同样的问题。有些东西持有对 DataPort 的引用。让我们再试一次:

//Code that starts the thread:
  Thread.Start(new WeakReference(this))
//. . .
public static void WorkerThreadMethod(object weakThisReference)

  DataPort strongThisReference= (DataPort)((WeakReference)weakThisReference).Target;

  //Extract the things we need from the parameter passed in (the DataPort)
  ManualResetEvent dataReady = strongThisReferencedataReady;
  WaitHandle closeSignal = strongThisReference.closeSignal;

  strongThisReference= null; //Forget the reference to the DataPort.

  for(;;)
  
    //Same as before, but without "this" . . .
  

现在我们可以了。 创建的 ThreadHelper 持有 WeakReference,这不会影响垃圾回收。我们只在工作线程开始时从 DataPort 中提取我们需要的数据,然后故意丢失对 DataPort 的所有引用。这在这个应用程序中是可以的,因为我们抓取的部分在 DataPort 的生命周期内不会改变。现在,当***应用程序丢失对 DataPort 的所有引用时,它就有资格进行垃圾收集。 GC 将运行终结器,该终结器将调用 Dispose 方法,该方法将杀死工作线程。一切都很幸福。

但是,这样做(或至少要正确)真的很痛苦!有没有更好的方法来制作一个拥有与该对象相同生命周期的线程的对象?或者,有没有办法在没有线程的情况下做到这一点?

结语: 如果不是让一个线程花费大部分时间来执行WaitHandle.WaitAny(),那就太好了,您可以拥有某种不需要它自己的线程的等待句柄,但会在线程池线程上触发一次延续它被触发了。就像,如果硬件 DLL 可以在每次有新数据时调用一个委托而不是发出事件信号,但我不控制那个 dll。

【讨论】:

您使用Thread 对象而不是Tasks 的任何原因? 这个问题基于早于 Tasks 的 .NET 框架版本,但我不确定 Tasks 是否能解决问题。我认为没有一种方法可以在没有事件循环的情况下等待硬件中断。我不控制非托管硬件 dll,所以我不能将它变成一个“推送”模型,在那里我给它一个委托,以便在有事件时调用它。 嗯,有道理。我认为 Tasks 不会解决您的问题;它们只是不那么繁琐,并且在某些情况下可以提高性能。由于框架决定了从线程池中分离出多少线程,而不是仅仅根据用户请求创建 n 个线程。正如您所说,更改为我不相信可以解决问题的任务。只是一个观察:)【参考方案2】:

我相信问题不在于您显示的代码,而在于使用此串行端口包装类的代码。如果那里没有“使用”语句,请参阅http://msdn.microsoft.com/en-us/library/yh598w02.aspx,您没有确定性的清理行为。相反,您然后依赖垃圾收集器,但它永远不会获得仍然被引用的对象,并且线程的所有堆栈变量(无论是作为普通参数还是 this 指针)都算作引用。

【讨论】:

对,DataPort 对象的 using 语句或调用 dispose 解决了这个问题,因为 Dispose 方法运行并杀死了工作线程。然而,我坚信即使你忘记调用 Dispose,垃圾收集器最终也应该得到它。 嗯。我不认为GC可以解决这个问题。查看en.wikipedia.org/wiki/…,您会看到堆栈或全局中的任何内容都被认为是可访问的。这适用于任何线程的堆栈,因此对 DataPort 的引用将使其保持活动状态。您可以尝试仅将 refs 传递给使用的句柄和重置事件,这会将其与其他事件解耦。

以上是关于线程防止所有者的垃圾收集的主要内容,如果未能解决你的问题,请参考以下文章

垃圾收集器

垃圾收集器

java虚拟机之垃圾收集器

JVM垃圾收集器

垃圾收集器

垃圾收集需要很长时间[如何调试收集的内容?]