在我创建的库中,我有一个类 DataPort,它实现了类似于 .NET SerialPort 类的功能。它与某些硬件对话,并在数据通过该硬件进入时引发事件。为实现此行为,DataPort 启动一个线程,该线程预期与 DataPort 对象具有相同的生命周期。 问题是当 DataPort 超出范围时,它永远不会被垃圾收集
现在,因为 DataPort 与硬件通信(使用 pInvoke)并拥有一些非托管资源,所以它实现了 IDisposable。当您在对象上调用 Dispose 时,一切都会正确发生。DataPort 摆脱了它所有的非托管资源并终止了工作线程并消失了。但是,如果您只是让 DataPort 超出范围,那么垃圾收集器将永远不会调用终结器,并且 DataPort 将永远在内存中保持活动状态。我知道发生这种情况有两个原因:
- 终结器中的断点永远不会被命中
- 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
- 上面提到的非托管资源不会影响问题。即使示例中的所有内容都使用托管资源,我仍然会看到相同的问题