在我创建的库中,我有一个 DataPort 类,它实现与 .NET SerialPort 类类似的功能。它与某些硬件进行通信,并且每当数据通过该硬件传入时就会引发一个事件。为了实现此行为,DataPort 启动一个线程,该线程预计具有与 DataPort 对象相同的生命周期。问题是当数据端口超出范围时,永远不会被垃圾收集
现在,由于 DataPort 与硬件通信(使用 pInvoke)并拥有一些非托管资源,因此它实现了 IDisposable。当您对对象调用 Dispose 时,一切都会正确发生。 DataPort 摆脱所有非托管资源并杀死工作线程并消失。但是,如果您只是让 DataPort 超出范围,则垃圾收集器将永远不会调用终结器,并且 DataPort 将永远在内存中保持活动状态。我知道发生这种情况有两个原因:
- 终结器中的断点永远不会被击中
-
SOS.dll告诉我数据端口仍然存在
Sidebar:在我们进一步讨论之前,我会说是的,我知道答案是“调用 Dispose() Dummy!”但我认为即使你让所有参考资料超出范围,正确的事情也应该发生最终并且垃圾收集器应该摆脱数据端口
回到问题:使用 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 串行端口类的一些内部实现细节
Update来自评论的一些额外信息:
- 有问题的线程已将 IsBackground 设置为 true
- 上面提到的非托管资源不会影响该问题。即使示例中的所有内容都使用托管资源,我仍然会看到相同的问题