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

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

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

  1. 终结器中的断点永远不会被击中
  2. SOS.dll告诉我DataPort仍然存在

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

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

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

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永远不会有资格进行垃圾回收。 因为永远不会调用终结器,所以我们永远不会向工作线程发送关闭信号。 因为工作线程永远不会得到关闭信号,所以它会永远持续下去并持有该引用。 ACK!

我能想到这个问题的唯一答案就是去除WorkerThread方法中的’this’参数(详见下面的答案)。 其他人可以考虑另一种选择吗? 必须有一种更好的方法来创建一个具有与对象相同生命周期的线程的对象! 或者,这可以在没有单独的线程的情况下完成吗? 我在msdn论坛上根据这篇文章选择了这个特定的设计,描述了常规.NET串口类的一些内部实现细节

从评论中更新一些额外信息:

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

为了摆脱隐含的“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,我看到仍有一个对ThreadHelper对象持有的DataPort的引用。 显然当你通过执行Thread.Start(this);启动工作线程时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方法,该方法将终止工作线程。 一切都很开心。

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

Epilogue:如果不是花费大部分时间花在WaitHandle.WaitAny()上,那么你可能会有一些不需要它自己的线程的等待句柄,但是会在Threadpool上触发一个延续,这样会很棒。线程一旦被触发。 比如,如果硬件DLL只是在每次有新数据时调用一个委托而不是发出事件信号,但我不控制那个dll。

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