【发布时间】:2013-03-19 17:28:25
【问题描述】:
在我创建的库中,我有一个 DataPort 类,它实现类似于 .NET SerialPort 类的功能。它与某些硬件对话,并在数据通过该硬件进入时引发事件。为实现此行为,DataPort 启动一个线程,该线程预期与 DataPort 对象具有相同的生命周期。 问题是当 DataPort 超出范围时,它永远不会被垃圾回收
现在,因为 DataPort 与硬件通信(使用 pInvoke)并拥有一些非托管资源,所以它实现了 IDisposable。当您在对象上调用 Dispose 时,一切都会正确发生。 DataPort 摆脱了它所有的非托管资源并终止了工作线程并消失了。但是,如果您只是让 DataPort 超出范围,那么垃圾收集器将永远不会调用终结器,并且 DataPort 将永远在内存中保持活动状态。我知道发生这种情况有两个原因:
- 终结器中的断点永远不会被命中
- SOS.dll 告诉我 DataPort 还活着
侧边栏:在我们继续之前,我要说是的,我知道答案是“调用 Dispose() Dummy!”但我认为,即使您让所有引用超出范围,最终应该会发生正确的事情,并且垃圾收集器应该摆脱 DataPort
回到问题:使用 SOS.dll,我可以看到我的 DataPort 没有被垃圾收集的原因是它启动的线程仍然具有对 DataPort 对象的引用- 通过线程正在运行的实例方法的隐式“this”参数。正在运行的工作线程will not be garbage collected,因此在运行工作线程范围内的任何引用也不符合垃圾回收条件。
线程本身基本上运行以下代码:
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 论坛上选择了这个基于 this post 的特殊设计,该论坛描述了常规 .NET 串行端口类的一些内部实现细节
更新来自 cmets 的一些额外信息:
- 有问题的线程已将 IsBackground 设置为 true
- 上面提到的非托管资源不会影响问题。即使示例中的所有内容都使用托管资源,我仍然会看到相同的问题
【问题讨论】:
-
您应该使用派生自
SafeHandle或CriticalHandle的类来包装您的非托管资源。如果您的库中的任何类的终结器没有扩展这两者之一,那么您可能有一个设计缺陷,这是一个等待发生的主要错误。当然也有例外,但它们非常罕见,以至于我已经有一段时间没有遇到过。这是a starting point,用于理解这些东西;如果您需要有关非托管清理的更多参考资料,请随时与我联系。 -
从这里记忆,但线程不会创建隐式 gc 根吗? (也许除非它们被设置为 isbackground?)
-
@280Z28 这个问题的 P/Invoke/unmanaged 部分可能不相关,但它在示例的第一部分中泄露了出来。唯一涉及的非托管资源是 dll 在 Open() 方法中返回的硬件句柄,我已经将其实现为 SafeHandle。 dataReady ManualResetEvent 被传递到非托管世界,但 P/Invoke 编组器负责处理。如果没有非托管资源,问题仍然会发生。 DataPort 不会被垃圾回收,它拥有的线程将永远存在。
-
@JerKimball 我相信有问题的线程已经将 IsBackground 设置为 true,因为它不会使进程保持活动状态,但我会仔细检查
标签: c# multithreading memory-leaks garbage-collection