Finalizer vs. Application: A Race Condition from Hell
One of my favorite managed debugging demos is analyzing a memory leak caused by a blocking finalizer. This tension between the finalizer thread and the application threads making allocations should be kept in mind by any programmer using this powerful but dangerous feature. However, there is another subtle category of bugs that can surface "thanks" to finalization: Race conditions between the finalizer and your application threads.
For example, consider the following scenario in which the File class acts as a wrapper over an implementation-provided Handle type:
class File : IDisposable
{
Handle h;
public File(string fn)
{
h = new Handle(fn);
}
public void Read()
{
Util.InternalRead(h);
}
~File()
{
h.Close();
}
public void Dispose()
{
h.Close();
GC.SuppressFinalize(this);
}
}
class Program
{
static void Main(string[] args)
{
File f = new File("1.txt");
f.Read();
}
}
The class' code seems perfectly solid even to the seasoned reviewer (although utterly useless :-)). It provides deterministic finalization through IDisposable, it provides a backup through a non-deterministic finalizer, it is very cautious to close the handle only once... The client code is a bit flaky as it doesn't call Dispose (nor uses the using statement), but the finalizer should take care of things. So what am I hinting at?
Due to the GC's eager nature, it's entirely possible for the file handle to be closed in the middle of the InternalRead operation. This is a very disturbing consequence that can be the source of extremely difficult debugging scenarios.
Once you have overcome the shock, let's follow the reasoning: The object itself is considered in use as long as it is referenced. Once it is no longer referenced, it becomes eligible for garbage collection and subsequently for finalization as well. In our case, the initial references are the local variable f in the main method, and the this implicit parameter (which is normally kept in the ECX register) in the Read method. The f local variable is not used after the f.Read() call, so it does not prevent the object from being collected. The surprise stems from the fact that the this parameter is not used after the file handle is passed to the InternalRead method, and therefore doesn't prevent the object from being collected either! Closing the handle in the middle of the InternalRead call is a bug from hell, and it's non-deterministic by definition because we have no idea when exactly the finalizer will be called.
Oh, and have I mentioned it? The bug will surface only in the Release build, because in the Debug build the GC treats local variables as active roots until the end of their enclosing scope to facilitate debugging. Did you need more motivation for running your tests against Release build as well?...