5

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

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

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

边栏:在我们继续之前,我会说是的,我知道答案是“调用 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 永远不符合垃圾收集的条件。因为永远不会调用终结器,所以我们永远不会向工作线程发送关闭信号。因为工作线程永远不会收到关闭信号,所以它会一直运行并保持该引用。确认!

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

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

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

2 回答 2

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" . . .
  }
}

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

回到 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" . . .
  }
}

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

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

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

于 2013-03-29T01:33:25.743 回答
0

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

于 2013-03-29T05:55:28.093 回答