Not so long ago we worked on a diagnostic rule related to the finalizer check. This provoked an argument on the details of the garbage collector work and the finalization of objects. Although we have been programming in C# for more than 5 years, we haven't achieved any consensus as regards this question, so I decided to study it more thoroughly.
Introduction
Usually .NET developers encounter a finalizer when they need to free an unmanaged resource. That's when a programmer has to think about a specific question: should we implement in our class IDisposable, or add a finalizer? Then he goes to Stack Overflow, for example, and reads answers to questions like the Finalize/Dispose pattern in C#, where he sees a classic pattern of IDisposable implementation, and the definition of the finalizer. The same pattern can be found in the MSDN description of the IDisposable interface. Some consider it quite complicated to understand, and offer other options like implementing the clearing of managed and unmanaged resources in separate methods, or creating a wrapper class especially for freeing unmanaged resource. You can find them on the same page on Stack Overflow.
Most of these methods suggest implementing a finalizer. Let's see what the benefits are, and what potential problems it can bring.
The pros and cons of using finalizers
Pros.
- A finalizer allows the clearing an object before it will be deleted by a garbage collector. If the developer forgot to call Dispose() method of an object, then it will be possible to free the unmanaged resources and thus, avoid the leak.
Well, that's it. That is the only plus, and it is quite controversial; we'll speak about the details later.
Cons.
- The finalization is not determined. You don't know when the finalizer will be called. Before CLR starts finalizing the objects, the garbage collector should place it in the queue of objects, ready for the finalization, when the next garbage collection starts. But this point is not defined.
- Due to the fact that an object with the finalizer does not get removed by the garbage collector immediately, the object, and the entire graph of dependent objects, go through the garbage collection and promote to the next generation. They will be removed only when the garbage collector decides to collect objects of this generation, which can take quite a while.
- Since the finalizers run in a separate thread in parallel with other threads of the application, a programmer may have a situation when the new objects, requiring finalization, will be created faster than the finalizers of old objects will complete the execution. This will lead to increased memory consumption, decreased performance, and perhaps eventually to the crash of the application with OutOfMemoryException. On the developer's machine you may never encounter this situation, for example because you have fewer processors, or the objects are created slower or the application does not work as long as it could and the memory doesn't run out as fast. It may take a lot of time to realize that the reason was the finalizers. Perhaps this minus outweighs the benefits of the only pro.
- If there is an exception during the finalizer execution, then the application will terminate. Therefore, if you implement a finalizer, you should be especially careful: do not access the methods of other objects for which the finalizer could be called; take into account that a finalizer is called in a separate thread; verify against null all other objects that could potentially be null. The last rule is related to the fact that the finalizer can be called for an object in any of its states, even incompletely initialized. For example, if you always assign in the constructor a new object in the class field and then expect that in the finalizer it should never be null and do access it, then you can get NullReferenceException, if there was an exception in the base class constructor during the creation of an object, and your constructor wasn't executed at all.
- A finalizer may be not executed at all. Upon the abortion of the application, for example, if there is an exception thrown in somebody's finalizer due to any of the reasons described above, no other finalizers will be executed. If you free unmanaged objects of the operating system, there will be nothing wrong in the way that the operating system returns its resources when the application terminates. But if you put unwritten bytes to the file, you will lose your data. So, perhaps it would be better not to implement the finalizer, but let the data be lost, in case you forgot to call Dispose(), because in this case the problem will be easier to find.
- We should remember that the finalizer is called only once, and if you resurrect the object in the finalizer by means of assigning a reference to it to a different live object, then perhaps, you should register it for the finalization again with the help of the method GC.ReRegisterForFinalize().
- You can face the problems of multithread applications; for example, the race condition, even if your application is single-threaded. This would be a very unusual case, but it is theoretically possible. Suppose there is a finalizer in your object, it is referenced by a different object that also has a finalizer. If both objects become eligible for garbage collection, and their finalizers start executing at the same time another object gets resurrected, then that object and your object become alive again. Now we may have a situation where the method of your object will be called from the main thread and from the finalizer at the same time, because it is still in the queue of objects, ready for the finalization. The code that reproduces this example is given below: You can see that first the finalizer of the Root object is executed, then the finalizer of the Nested object, and then the method DoSomeWork() is called from two threads at the same time.
class Root
{
public volatile static Root StaticRoot = null;
public Nested Nested = null;
~Root()
{
Console.WriteLine("Finalization of Root");
StaticRoot = this;
}
}
class Nested
{
public void DoSomeWork()
{
Console.WriteLine(String.Format(
"Thread {0} enters DoSomeWork",
Thread.CurrentThread.ManagedThreadId));
Thread.Sleep(2000);
Console.WriteLine(String.Format(
"Thread {0} leaves DoSomeWork",
Thread.CurrentThread.ManagedThreadId));
}
~Nested()
{
Console.WriteLine("Finalization of Nested");
DoSomeWork();
}
}
class Program
{
static void CreateObjects()
{
Nested nested = new Nested();
Root root = new Root();
root.Nested = nested;
}
static void Main(string[] args)
{
CreateObjects();
GC.Collect();
while (Root.StaticRoot == null) { }
Root.StaticRoot.Nested.DoSomeWork();
Console.ReadLine();
}
}
This is what will be be displayed on my machine:
Finalization of Root
Finalization of Nested
Thread 10 enters DoSomeWork
Thread 2 enters DoSomeWork
Thread 10 leaves DoSomeWork
Thread 2 leaves DoSomeWork
If your finalizers called in a different order, try to change the places of the creation of nested and root.
Conclusion
Finalizers in .NET are the easiest way to shoot yourself in the foot. Before you rush into adding finalizers for all the classes that are implementing IDisposable, think first; do you really need them that much? It should be noted that the CLR developers warn against their use on the page the Dispose Pattern: "Avoid making types finalizable. Carefully consider any case in which you think a finalizer is needed. There is a real cost associated with instances with finalizers, from both a performance and code complexity standpoint."
But if you decide to use finalizers anyway, PVS-Studio will help you find potential bugs. We have the V3100 diagnostic, which can indicate all spots in the finalizer where there is a possibility of NullReferenceException.