Sasha Goldshtein is a Senior Consultant for Sela Group, an Israeli company specializing in training, consulting and outsourcing to local and international customers.Sasha's work is divided across these three primary disciplines. He consults for clients on architecture, development, debugging and performance issues; he actively develops code using the latest bits of technology from Microsoft; and he conducts training classes on a variety of topics, from Windows Internals to .NET Performance. You can read more about Sasha's work and his latest ventures at his blog: http://blogs.microsoft.co.il/blogs/sasha. Sasha writes from Herzliya, Israel. Sasha is a DZone MVB and is not an employee of DZone and has posted 202 posts at DZone. You can read more from them at their website. View Full User Profile

.NET Internals : Guaranteed, Finalization Order Is Not

08.09.2008
| 3596 views |
  • submit to reddit
As part of our ongoing quest to categorize all finalization-related problems, I'd like to present another honorable mention in the series.  This time, it's finalization order (or the lack thereof).

A quick refresher on finalization: When an object with a finalizer is created, a reference to it is placed in the finalization queue.  When the object is no longer referenced by the application, the GC moves it to the f-reachable queue.  This wakes up the finalizer thread, which in turn removes the object from the queue and runs its finalizer.  At the next GC, the object's memory is reclaimed.

With that in mind, consider a graph of objects with finalizers, such as a StreamWriter and a FileStream.  The FileStream is a thin wrapper around a Win32 file handle and the related APIs.  The StreamWriter is a buffering convenience wrapper around a stream.  Both require finalization to work properly - the StreamWriter needs a finalizer to flush its internal buffer and close the underlying stream, and the FileStream needs a finalizer to close the Win32 file handle.  Both should also implement IDisposable or provide other means for deterministic finalization (e.g. a Close method).

So what happens if the user doesn't use deterministic finalization and relies on a finalizer instead?

FileStream fs = new FileStream("1.txt", FileAccess.ReadWrite);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine("Hello World");
//No sw.Close() here

Scenario #1: If the StreamWriter's finalizer is called first, it will flush the buffer and close the FileStream.  Since the stream was deterministically closed, it will not be finalized.

Scenario #2: If the FileStream's finalizer is called first, it will close the Win32 file handle.  When the StreamWriter's finalizer is called, it will attempt to flush its internal buffer into a closed stream!

Since finalization order is undefined (and there's no way for it to be defined, because it depends on the GC's traversal order which is also undefined), we can never tell if we're going to land in scenario #1 (yay) or scenario #2 (ouch).  Therefore, StreamWriter does not have a finalizer.

Bonus question: What happens if you forget to deterministically close a StreamWriter?  (You can try it at home.  Nothing horrible will happen, but you will lose the buffered data that wasn't yet flushed to the underlying stream.)

This also brings up the subject of resurrection, a little-known "feature" that boils down to making an object accessible from within a finalizer.  For example, if we were to implement object pooling, we would want to ensure that even if the user didn't explicitly return the instance to the pool, it will still be returned to the pool by the finalizer:

class PooledObject
{
//Implementation omitted for clarity
//...

~PooledObject()
{
Pool.ReturnToPool(this);
}
}

The first problem with this approach is that the finalizer for this specific instance won't ever be called again.  We get one shot at the finalization queue - no one is going to add our instance back automatically, so we have to take care of it ourselves:

GC.ReRegisterForFinalize(this);

Another problem we are certainly going to have revolves around other finalizable objects we might be referencing.  Since finalization order is undefined, it is entirely possible that the finalizers for our contained (referenced) objects have already run, rendering their state inconsistent.  Any attempt to use them will yield undefined results - few objects are willing to work properly after their finalizer has already run, or are even aware of the possibility this might happen!

Effectively, the only "safe" way of resurrecting such referenced objects that are outside our control is discarding and reinitializing them from scratch.

References
Published at DZone with permission of Sasha Goldshtein, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)