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

本文关键字:线程 所有者 | 更新日期: 2023-09-27 18:06:20

在我创建的库中,我有一个类DataPort,它实现了类似于. net SerialPort类的功能。它与一些硬件对话,并在数据通过该硬件进入时引发一个事件。为了实现这种行为,datapport启动了一个线程,该线程预计与datapport对象具有相同的生存期。问题是当datapport超出作用域时,它永远不会被垃圾收集

现在,由于DataPort与硬件(使用pInvoke)对话并拥有一些非托管资源,因此它实现了IDisposable。当你在对象上调用Dispose时,一切都会正确发生。datapport将清除其所有非托管资源,并杀死工作线程并离开。但是,如果您只是让datapport超出作用域,那么垃圾收集器将永远不会调用终结器,并且datapport将永远在内存中存活。我知道发生这种情况有两个原因:

    终止器中的断点永远不会被击中
  1. SOS.dll告诉我数据端口还活着

边栏:在我们进一步讨论之前,我会说是的,我知道答案是"调用Dispose() Dummy!"但我认为即使您让所有引用超出作用域,正确的事情也应该发生最终和垃圾收集器应该摆脱DataPort

回到问题:使用SOS.dll,我可以看到我的datapport没有被垃圾收集的原因是因为它启动的线程仍然有对datapport对象的引用-通过线程正在运行的实例方法的隐式"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根线程,并且它持有对datapport的引用,所以datapport永远不适合垃圾收集。因为终结器永远不会被调用,所以我们永远不会向工作线程发送关闭信号。因为工作线程从来没有得到关闭信号,所以它会一直运行并持有该引用。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对象持有。显然,当您通过执行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,这不会影响垃圾收集。我们只在工作线程开始时从datapport提取我们需要的数据,然后故意丢失对datapport的所有引用。这在这个应用程序中没有问题,因为我们获取的部分在datapport的生命周期内不会改变。现在,当顶级应用程序失去对datapport的所有引用时,它就有资格进行垃圾收集。GC将运行终结器,它将调用Dispose方法,从而杀死工作线程。一切都是幸福的。

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

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

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