线程阻止所有者的垃圾回收 [英] Thread preventing garbage collection of owner

查看:103
本文介绍了线程阻止所有者的垃圾回收的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我创建的库中,我有一个类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(;;)
{
//等到这里有数据,或者我们有信号终止线程,因为我们正在处理
int signalIndex = WaitHandle .WaitAny(new WaitHandle [] {this.dataReady,this.closeSignal});
if(signalIndex == 1)// closeSignal在索引1
{
// //我们得到了关闭信号。我们正在处理!
return; //这将停止线程
}
else
{
//一定是来自硬件的dataReady信号而不是关闭信号。
this.ProcessDataFromHardware();
dataReady.Reset()
}
}
}



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

  public void Dispose()
{
closeSignal.Set();
workerThread.Join();





$ b因为该线程是一个gc根,并且它拥有对DataPort的引用, DataPort永远不符合垃圾回收的条件。因为永远不会调用终结器,所以我们绝不会将闭合信号发送给工作线程。因为工作者线程永远不会收到关闭信号,所以它会一直持续下去并持有该引用。 ACK!


我能想到的唯一答案就是摆脱WorkerThread方法中的'this'参数(详见下面的答案)。其他人能否想到另一种选择?必须有一种更好的方法来创建一个具有与该对象相同生命周期的线程的对象!另外,这可以做到没有一个单独的线程?我选择了基于这篇文章在MSDN论坛上介绍了常规.NET串行端口类的一些内部实现细节。



更新




  • 有问题的线程将IsBackground设置为true

  • 上述非托管资源不会影响问题。即使示例中的所有内容都使用了托管资源,我仍然会看到同样的问题。
  • 解决方案

为了摆脱隐含的This参数,我改变了工作线程方法,并将this引用作为参数传入:

  public static void WorkerThreadMethod(object thisParameter)
{
//从传入的参数(DataPort)中提取我们需要的东西
// dataReady曾经是' this.dataReady'和closeSignal曾经是
//'this.closeSignal'
ManualResetEvent dataReady =((DataPort)thisParameter).dataReady;
WaitHandle closeSignal =((DataPort)thisParameter).closeSignal;

thisParameter = null; //忘记对(;;)
{
// DataPort

的引用,与之前一样,但没有this。 。 。令人震惊的是,这并没有解决问题,但是,这并不能解决问题。 !回到SOS.dll,我看到仍然有一个引用我的DataPort被一个ThreadHelper对象保存着。
显然,当通过执行 Thread.Start(this); 来启动一个工作线程时,它会创建一个私有的ThreadHelper对象,其生命周期与保存到引用你传入了Start方法(我推断)。这给我们带来了同样的问题。某物正在持有对DataPort的引用。让我们再试一次:

  //开始线程的代码:
Thread.Start(new WeakReference这))
//。 。 。
public static void WorkerThreadMethod(object weakThisReference)
{
DataPort strongThisReference =(DataPort)((WeakReference)weakThisReference).Target;

//从传入的参数(DataPort)中提取我们需要的东西
ManualResetEvent dataReady = strongThisReferencedataReady;
WaitHandle closeSignal = strongThisReference.closeSignal;

strongThisReference = null; //忘记对DataPort的引用。 $;
$ b为(;;)
{
// //同前,但没有this。 。 。


$ / code $ / pre
$ b $ p现在我们没事了。 strong>创建的ThreadHelper保存到WeakReference中,这不会影响垃圾回收。我们只从工作线程开始的DataPort中提取需要的数据,然后故意丢失对DataPort的所有引用。在这个应用程序中这是可以的,因为我们所抓取的部分在DataPort的生命周期中不会改变。现在,当顶级应用程序丢失对DataPort的所有引用时,它就有资格进行垃圾回收。 GC将运行终结器,它将调用Dispose方法来终止工作者线程。一切都很开心。



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



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


In a library I've created, I have a class, DataPort, that implements functionality similar to the .NET SerialPort class. It talks to some hardware and will raise an event whenever data comes in over that hardware. To implement this behavior, DataPort spins up a thread that is expected to have the same lifetime as the DataPort object. The problem is that when the DataPort goes out of scope, it never gets garbage collected

Now, because DataPort talks to hardware (using pInvoke) and owns some unmanaged resources, it implements IDisposable. When you call Dispose on the object, everything happens correctly. The DataPort gets rid of all of its unmanaged resources and kills the worker thread and goes away. If you just let DataPort go out of scope, however, the garbage collector will never call the finalizer and the DataPort will stay alive in memory forever. I know this is happening for two reasons:

  1. A breakpoint in the finalizer never gets hit
  2. SOS.dll tells me that the DataPort is still alive

Sidebar: Before we go any further, I'll say that yes, I know the answer is "Call Dispose() Dummy!" but I think that even if you let all references go out of scope, the right thing should happen eventually and the garbage collector should get rid of the DataPort

Back to the Issue: Using SOS.dll, I can see that the reason my DataPort isn't being garbage collected is because the thread that it spun up still has a reference to the DataPort object - through the implicit "this" parameter of the instance method that the thread is running. The running worker thread will not be garbage collected, so any references that are in the scope of the running worker thread are also not eligible for garbage collection.

The thread itself runs basically the following code:

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()
    }
  }
}

The Dispose method contains the following (relevant) code:

public void Dispose()
{
  closeSignal.Set();
  workerThread.Join();
}

Because the thread is a gc root and it holds a reference to the DataPort, the DataPort is never eligible for the garbage collection. Because the finalizer is never called, we never send the close signal to the worker thread. Because the worker thread never gets the close signal, it keeps going forever and holding that reference. ACK!

The only answer I can think of to this problem is to get rid of the 'this' parameter on the WorkerThread method (detailed below in the answers). Can anybody else think of another option? There must be a better way to create an object with a thread that has the same lifetime of the object! Alternatively, can this be done without a separate thread? I chose this particular design based on this post over at the msdn forums that describe some of the internal implementation details of the regular .NET serial port class

Update a bit of extra information from the comments:

  • The thread in question has IsBackground set to true
  • The unmanaged resources mentioned above don't affect the problem. Even if everything in the example used managed resources, I would still see the same issue

解决方案

To get rid of the implicit "This" parameter, I changed the worker thread method around a bit and passed in the "this" reference as a parameter:

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

Shockingly, this did not solve the problem!

Going back to SOS.dll, I saw that there was still a reference to my DataPort being held by a ThreadHelper object. Apparently when you spin up a worker thread by doing Thread.Start(this);, it creates a private ThreadHelper object with the same lifetime as the thread that holds onto the reference that you passed in to the Start method (I'm inferring). That leaves us with the same problem. Something is holding a reference to DataPort. Let's give this one more try:

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

Now we're OK. The ThreadHelper that gets created holds onto a WeakReference, which won't affect garbage collection. We extract only the data we need from the DataPort at the beginning of the worker thread and then intentionally lose all references to the DataPort. This is OK in this application because the parts of it that we grab don't change over the lifetime of the DataPort. Now, when the top level application loses all references to the DataPort, it's eligible for garbage collection. The GC will run the finalizer which will call the Dispose method which will kill the worker thread. Everything is happy.

However, this is a real pain to do (or at least get right)! Is there a better way to make an object that owns a thread with the same lifetime as that object? Alternatively, is there a way to do this without the thread?

Epilogue: It would be great if instead of having a thread that spends most of its time doing WaitHandle.WaitAny(), you could have some sort of wait handle that didn't need it's own thread, but would fire a continuation on a Threadpool thread once it's triggered. Like, if the hardware DLL could just call a delegate every time there's new data instead of signaling an event, but I don't control that dll.

这篇关于线程阻止所有者的垃圾回收的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆