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

Pet*_*man 5 c# multithreading garbage-collection memory-leaks

在我创建的库中,我有一个类DataPort,它实现类似于.NET SerialPort类的功能.它与某些硬件进行通信,并且只要数据通过该硬件进入就会引发事件.为了实现此行为,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()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

public void Dispose()
{
  closeSignal.Set();
  workerThread.Join();
}
Run Code Online (Sandbox Code Playgroud)

因为该线程是一个gc root并且它拥有对DataPort的引用,所以DataPort永远不会有资格进行垃圾回收.因为永远不会调用终结器,所以我们永远不会向工作线程发送关闭信号.因为工作线程永远不会得到关闭信号,所以它会一直持续并保持该引用.ACK!

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

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

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

Pet*_*man 4

为了摆脱隐式的“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" . . .
  }
}
Run Code Online (Sandbox Code Playgroud)

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

回到 SOS.dll,我看到 ThreadHelper 对象仍然保留着对我的 DataPort 的引用。显然,当您通过执行以下操作来启动工作线程时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" . . .
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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