5

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
Community
  • 1
  • 1
Pete Baughman
  • 2,996
  • 2
  • 19
  • 33
  • You should use classes derived from `SafeHandle` or `CriticalHandle` to wrap your unmanaged resources. If any class in your library has a finalizer that does not extend one of those two, you *probably* have a design flaw that's a major bug waiting to happen. There are exceptions of course, but they are rare enough that I haven't faced one in quite a while. Here's [a starting point](http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae) for understanding this stuff; feel free to contact me if you want additional references regarding unmanaged cleanup. – Sam Harwell Mar 29 '13 at 02:26
  • Going from memory here, but don't threads create implicit gc roots? (maybe unless they're set as isbackground?) – JerKimball Mar 29 '13 at 03:26
  • @280Z28 The P/Invoke/unmanaged portion of this problem is probably not relevant, but it leaked out in the first part of the example. The only unmanaged resource involved is the handle to the hardware that the dll returns in the Open() method which I already implements as a SafeHandle. The dataReady ManualResetEvent gets passed to the unmanaged world, but the P/Invoke marshaller takes care of that. The problem would still occur without the unmanaged resources. DataPort wouldn't get garbage collected and the thread that it owns would live forever. – Pete Baughman Mar 29 '13 at 04:42
  • @JerKimball I believe the thread in question already sets IsBackground to true because it won't keep the process alive, but I'll double check – Pete Baughman Mar 29 '13 at 04:46

2 Answers2

4

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.

Pete Baughman
  • 2,996
  • 2
  • 19
  • 33
  • Any reason why you are using `Thread` objects and not `Tasks`? – Johnathon Sullinger May 31 '14 at 06:31
  • This question was based on a version of the .NET framework that predates Tasks, but I'm not sure that Tasks solve the problem. I don't think there's a way to wait on the hardware interrupts without an event loop. I don't control the unmanaged hardware dlls, so I can't turn it into a 'push' model where I give it a delegate to call when there's an event. – Pete Baughman May 31 '14 at 06:36
  • Ah ok, it makes sense. I don't think Tasks would solve your issue; they're just less cumbersome and in some cases improve performance. Since the framework determines how many threads to spin off from the thread pool instead of just creating _n_ number of threads at the users request. As you said though, changing to Tasks I don't believe would solve the issue. Just an observation :) – Johnathon Sullinger May 31 '14 at 06:38
0

I believe that the problem is not in the code you have shown but in the code using this serial port wrapper class. If you don't have a "using" statement there, see http://msdn.microsoft.com/en-us/library/yh598w02.aspx, you don't have deterministic cleanup behaviour. Instead, you then rely on the garbage collector but that will never reap an object that is still referenced, and all stack-variables of a thread (whether as normal parameter or this-pointer) count as references.

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • Right, the using statement or calling dispose on the DataPort object fixes the issue, because the Dispose method runs and kills the worker thread. I'm a firm believer, however, that even if you forget to call Dispose, the garbage collector should get it eventually. – Pete Baughman Mar 29 '13 at 13:37
  • Hmm. I don't think the GC can solve that. Looking at http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Reachability_of_an_object, you will see that anything on the stack or global is considered reachable. This applies to the stack of any thread, so having a reference to the DataPort there will keep it alive. You could try to only pass in refs to the used handle and reset event, which would decouple it from the rest. – Ulrich Eckhardt Mar 29 '13 at 14:20