Monday, December 03, 2007

Where Did My Object Go? Part 2

In Part 1 of this article I discussed the possibility of an object instance being collected before a method returns in this scenario:

    new MyObject().LongRunningMethod();

as well as this scenario:

    MyObject o = new MyObject();
o.LongRunningMethod();

Here we'll discuss how this could become a problem. Frankly, it's pretty easy to cause the problem, but I think it generally involves some ugliness on the part of software design or implementation, largely involving clean-up of fields when the instance is finalized.


Don't Try This at Home


I don't think it's likely you'll see a lot of code that does this, but here's one way to run afoul of your object going way: export a reference to a field that the object cleans up when it's finalized. You can export the reference, say, by making that field visible through a property. Exposing a field that you're going to clean up in the finalizer would be an easy way to create a coupling between your object and other (arbitrary) client code that has no knowledge of your object's life span.


Call Stack Antics


Another way to invoke the potential problem is to lose your "this" reference on the call stack. The code below manages to lose the "this" reference by passing a field to the helper method rather than allowing the helper to access the field directly via its own "this" reference. When the helper tries to access fs.Length an ObjectDisposedException is thrown.


Why does this throw? Well, the last live reference to the instance was lost when LongRunningMethod passed _input to Helper. Essentially we've again exported the field value from the instance and no longer hold a reference to the instance, allowing the GC to finalize it. Helper is left holding a reference to an object that has been finalized.


Note: Again, you will not see this behavior in a debug build. When running code marked as "debug" the JITter extends the lifetime of the local object to the end of the method. So you will not see this effect if you've compiled with the /debug flag.

    using System;
using System.IO;

// Ugliness ensues.
sealed class MyUglyObject
{
public MyUglyObject(string inputPath)
{
// Real production code would likely not delete the file when done...
// ...but this is a sample app.
_input = new FileStream(Path.GetTempFileName(), FileMode.Open, FileAccess.Read, FileShare.Read);
}

~MyUglyObject()
{
if (_input != null)
{
_input.Close();
_input = null;
}
}

public void LongRunningMethod()
{
Helper(_input); // Our last reference to 'this' (implicit).
}

private void Helper(FileStream fs)
{
// A long-running method can easily experience a garbage collection
// before returning. This one happens for force it to occur.
GC.Collect();
GC.WaitForPendingFinalizers();

// Ka-boom!
long inputSize = fs.Length;
// ...
}

private FileStream _input;
}

class Program
{
static void Main(string[] args)
{
new MyUglyObject(@"..\..\readme.txt").LongRunningMethod();
}
}

Can You See It?


As you can see above it takes a bit of effort to cause the code to blow up. If Helper had used _input.Length instead of taking a parameter the problem would not exist.


But, what I find a bit creepy about the above code is that if Helper were a static method it would seem respectable:

    private static void Helper(FileStream fs)
{
// A long-running method can easily experience a garbage collection
// before returning. This one happens for force it to occur.
GC.Collect();
GC.WaitForPendingFinalizers();

// Ka-boom!
long inputSize = fs.Length;
// ...
}

At first glance it now looks like helper is a normal static helper function, as is likely seen in code bases across the world. It doesn't need a "this" reference, it takes a reference to the object it uses, everything appears fine on the surface. Would you see the "this" reference being lost by the code calling Helper in a code review? I'm not so sure I would have until recently.


 


 


Technorati tags: , , , ,

3 comments:

Anonymous said...

Note that FxCop would flag Helper as not using "this" and thus as a candidate for being static.

Isn't the guideline that Dispose should cleanup managed and unmanaged resources, while the finalizer should only clean up unmanaged resources? The above code is breaking that guideline by cleaning up managed resources in the finalizer, which causes the problem.

If your code is using an unmanaged resource whose lifetime is managed by a class with a finalizer it migh be something to beware of though.

Curt said...

Hi, Sven.

Yes, this is a fabricated example that breaks rules, but does an adequate job at illustrating the phenomenon I wanted to discuss.

re FxCop, whether the Helper method is a static or instance method doesn't change the behavior.

Sela said...

I think there's a simpler scenario with much worse results.

In the long running method you can use a value type field (e.g. int) but since the object has already been collected its memory may be allocated now for a new object. This object may change the memory of this field and the long running method may also change it. If this field is now a reference in the new object this may cause an invalid pointer usage possibly crashing the process.

This scenario however is very hard to produce intentionally.