GOTCHA 36 Releasing managed resources in Finalize( ) can wreak havoc


GOTCHA #36 Releasing managed resources in Finalize( ) can wreak havoc

Do not access managed resources within the Finalize() method. Typically objects may have dependencies on other objects. In a language like C++, it is not unusual within a destructor to communicate with associated objects. However, when working with .NET, you should not carry those practices over to the Finalize() method.

You may ask, "So what if I access other objects within Finalize()?" The reason you shouldn't is that the result cannot be predicted. You have no idea of the order in which the garbage collector will call the Finalize() method of your objects. You may write code that accesses other objects within Finalize(), run your program, and say, "See, it works!" The phrase "it works" is a very unpleasant one in programming. It's like saying, "See, I drove the wrong direction on a one-way street and nothing went wrong." In such cases, it is not a question of if things will go wrong, but when. This is illustrated in Example 5-2.

Example 5-2. Accessing managed resources

C# (NotToFinalize)

 //SomeOtherClass.cs using System; namespace FinalizeAndManagedResources {     public class SomeOtherClass     {         private int[] values = new int[1000000];         public void Notify()         {             Console.WriteLine("Notify called on SomeOtherClass");         }         ~SomeOtherClass()         {             Console.WriteLine("*** SomeOtherClass Finalized");         }     } } //SomeClass.cs using System; namespace FinalizeAndManagedResources {     public class SomeClass     {         private SomeOtherClass other;         public void Setother(SomeOtherClass otherObject)         {             other = otherObject;         }         ~SomeClass()         {             Console.WriteLine("Finalize called on SomeClass");             if(other != null)             {         other.Notify();             }         }     } } //Test.cs using System; namespace FinalizeAndManagedResources {     class Test     {         [STAThread]         static void Main(string[] args)         {     SomeClass object1 = new SomeClass();     SomeOtherClass object2 = new SomeOtherClass();     object1.Setother(object2);         }     } } 

VB.NET (NotToFinalize)

 'SomeOtherClass.vb Public Class SomeOtherClass     Private values() As Integer = New Integer(1000000 - 1) {}     Public Sub Notify()         Console.WriteLine("Notify called on SomeOtherClass")     End Sub     Protected Overrides Sub Finalize()         MyBase.Finalize()         Console.WriteLine("*** SomeOtherClass Finalized")     End Sub End Class 'SomeClass.vb Public Class SomeClass     Private other As SomeOtherClass     Public Sub Setother(ByVal otherObject As SomeOtherClass)         other = otherObject     End Sub     Protected Overrides Sub Finalize()         MyBase.Finalize()         Console.WriteLine("Finalize called on SomeClass")         If Not other Is Nothing Then     other.Notify()         End If     End Sub End Class 'Test.vb Module Test     Sub Main()         Dim object1 As SomeClass = New SomeClass         Dim object2 As SomeOtherClass = New SomeOtherClass         object1.Setother(object2)     End Sub End Module 

In this example, an instance of SomeClass has a reference to an instance of SomeOtherClass. When the object of SomeClass is finalized, it calls the Notify() method on the SomeOtherClass object. In the Main() method of the Test module, you create instances of the two classes and relate them using the SetOther() method of SomeClass. When the program completes execution, both the objects are garbage collected. The question is, in which order?

Figure 5-2 shows one possible scenario. (One of my reviewers asked how many times I had to run the program to get this output. It took only one run!)

The Notify() method is called on the object after its Finalize() method has already executed. This is strange behavior and may lead to unpredictable results. Had it thrown an exception or crashed, it would be easy to realize the problem and fix it. Because it looks like it works, the problem remains obscured and unfixed.

Figure 5-2. Output from Example 5-2


Here's another issue to consider. Suppose the method called during finalization is trying to be a good thread-safe citizen. Take a look at Example 5-3. Though this example is somewhat contrived, it starkly illustrates the risks of accessing managed resources in Finalize().

Example 5-3. Inadvertently locking from within Finalize()

C# (NotToFinalize)

 //SomeOtherClass.cs using System; namespace FinalizeAndManagedResources {     public class SomeOtherClass     {         private int[] values = new int[1000000];         public void Notify()         {             Console.WriteLine("Entering Notify");     lock(this)             {                 Console.WriteLine("Notify called on SomeOtherClass");             }         }         ~SomeOtherClass()         {             Console.WriteLine("*** SomeOtherClass Finalized");         }     } } //SomeClass.cs using System; namespace FinalizeAndManagedResources {     public class SomeClass     {         private SomeOtherClass other;         public void Setother(SomeOtherClass otherObject)         {             other = otherObject;         }         ~SomeClass()         {             Console.WriteLine("Finalize called on SomeClass");             if(other != null)             {         other.Notify();             }         }     } } //Test.cs using System; namespace FinalizeAndManagedResources {     class Test     {         private static void ohoh(SomeOtherClass obj)         {             SomeClass object1 = new SomeClass();             object1.Setother(obj);         }         [STAThread]         static void Main(string[] args)         {             SomeOtherClass object2 = new SomeOtherClass();             lock(object2)             {                 ohoh(object2);                 GC.Collect();                 //GC.WaitForPendingFinalizers();                 Console.WriteLine("OK let's release lock in Main");             }             Console.WriteLine("Are we here");         }     } } 

VB.NET (NotToFinalize)

 'SomeOtherClass.vb Public Class SomeOtherClass     Private values() As Integer = New Integer(1000000 - 1) {}     Public Sub Notify()         Console.WriteLine("Entering Notify")         SyncLock Me             Console.WriteLine("Notify called on SomeOtherClass")         End SyncLock     End Sub     Protected Overrides Sub Finalize()         MyBase.Finalize()         Console.WriteLine("*** SomeOtherClass Finalized")     End Sub End Class 'SomeClass.vb Public Class SomeClass     Private other As SomeOtherClass     Public Sub Setother(ByVal otherObject As SomeOtherClass)         other = otherObject     End Sub     Protected Overrides Sub Finalize()         MyBase.Finalize()         Console.WriteLine("Finalize called on SomeClass")         If Not other Is Nothing Then     other.Notify()         End If     End Sub End Class 'Test.vb Module Test     Sub ohoh(ByVal obj As SomeOtherClass)         Dim object1 As New SomeClass         object1.Setother(obj)     End Sub     Sub Main()         Dim object2 As New SomeOtherClass         SyncLock object2             ohoh(object2)             GC.Collect()             'GC.WaitForPendingFinalizers()             Console.WriteLine("OK let's release lock in Main")         End SyncLock         Console.WriteLine("Are we here")     End Sub End Module 

Within the Main() method you get a lock on an object of SomeOtherClass. Then you create an instance of SomeClass and the instance of SomeOtherClass is associated with it. When GC.Collect() is invoked, since the instance of SomeClass is no longer accessible, its Finalize() is called. Within this Finalize() method, you invoke Notify() on the instance of SomeOtherClass. In Notify(), you lock the object. However, the main thread within the Main() method already owns a lock on the object. This blocks the garbage collector threadit cannot continue until the Main() method completes. (Main() is able to run to completion because the call to GC.Collect() does not block, even though the CLR's finalization thread does.) You can see this in the output from the program, shown in Figure 5-3.

Figure 5-3. Output from Example 5-3


Let's take this a bit further. The GC class's WaitForPendingFinalizers() method suspends the calling thread until the finalization thread has run the Finalize() method on all eligible objects in its queue. Now what happens if you uncomment the GC.WaitForPendingFinalizers() in the Main() method in Example 5-3? The output is shown in Figure 5-4.

Figure 5-4. Deadlock after GC.WaitForPendingFinalizers() was uncommented in Example 5-3


The program is deadlocked. You may say, well, don't call GC.WaitForPendingFinalizers(), or GC.Collect() for that matter. But what if you had called some other method on the object from another thread, and that thread holds a lock on the object while it waits for something else? There is a high probability of deadlock occurring if you access managed resources within the Finalize() method.

IN A NUTSHELL

Do not touch any managed objects within the Finalize() method. Do not call any methods on other objects, or even set their references to null/Nothing. This could lead to unpredictable results, including the possibility of crash or deadlock. Only release unmanaged resources within Finalize().

SEE ALSO

Gotcha #35, "Writing Finalize( ) is rarely a good idea," Gotcha #37, "Rules to invoke base.Finalize( ) are not consistent," Gotcha #38, "Depending on Finalize() can tie up critical resources," Gotcha #39, "Using Finalize() on disposed objects is costly," Gotcha #40, "Implementing IDisposable isn't enough," and Gotcha #41, "Using the Dispose Design Pattern doesn't guarantee cleanup."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net