📜 ⬆️ ⬇️

Why you should not use finalizers

Not so long ago, we worked on diagnostics related to checking the finalizer, and my colleague and I had a dispute over the details of the work of the garbage collector and the finalization of objects. And although he and I have been working on C # for more than 5 years, we have not come to a common opinion, and I decided to study this question in more detail.



Introduction


Usually, the first acquaintance with finalizers from .NET developers occurs when they need to release an unmanaged resource. The question arises: what should I use: implement IDisposable in my class or add a finalizer? Then they go, for example, to StackOverflow and read the answers to questions like this Finalize / Dispose pattern in C # which tells about the classic pattern of the IDisposable implementation in combination with the definition of the finalizer. The same pattern can be found in MSDN in the IDisposable interface description. Some people find it quite difficult to understand and offer their own options, such as implementing the cleaning of managed and unmanaged resources in separate methods or creating a wrapper class specifically to release an unmanaged resource. They can be found on the same page on StackOverflow.

Most of these methods involve the implementation of the finalizer. Let's see what advantages and potential problems this can bring.
')

Pros and cons of using finalizers


Pros.

  1. The finalizer allows you to clean the object before it is removed by the garbage collector. If the developer has forgotten to call Dispose () on an object, then in the finalizer you can release unmanaged resources and thus avoid their leakage.

Perhaps that's all. This is the only plus, and even then controversial, as discussed below.

Minuses.
  1. Finalization is non-deterministic. You do not know when the finalizer will be called. Before the CLR starts to finalize the objects, the garbage collector must place them in a queue of objects ready for finalization when the next garbage collection starts. And this moment is not defined.

  2. Due to the fact that the object with the finalizer is not removed by the garbage collector immediately, he and the entire graph of related objects survive the garbage collection and get into the next generation. They will be removed now when the garbage collector decides to collect objects of this generation, which may happen very soon.

  3. Since finalizers are executed in a separate thread in parallel with the work of other application threads, a situation may arise when new objects requiring finalization can be created faster than finalizers of old objects will work. This will lead to an increase in memory consumption, a decrease in performance and, possibly, in the end, to an application crash with an OutOfMemoryException . And on the developer's machine, you may never encounter this situation, for example, because you have a smaller number of processors and objects are created more slowly or the application does not work as long as in combat conditions, and the memory does not have time to run out. You can spend a lot of time trying to figure out that the reason was in the finalizers. This minus, perhaps, covers the advantages of a single plus.

  4. If an exception occurs during the execution of the finalizer, the application will terminate urgently. Therefore, when implementing a finalizer, you need to be especially careful: do not refer to the methods of other objects for which the finalizer could already be called; take into account that the finalizer is called in a separate thread; check for null all other objects that could potentially be null . The last rule is connected with the fact that the finalizer can be invoked for an object in any of its states, not even fully initialized. For example, if you always assign a new object in the class field in the constructor and then expect that it must not always be null in the finalizer and access it, you can get a NullReferenceException if you create an object in the base class constructor and an exception occurs before executing your designer is not reached.

  5. Finalizer may not be executed at all. In case of emergency completion of the application, for example, when an exception occurs in a foreign finalizer for the reasons described in the previous paragraph, all other finalizers will not be executed. If you release unmanaged operating system objects in the finalizer, then nothing bad will happen in the sense that when the application is completed, the system will return its resources. But if you drop the underwritten bytes into a file, you will lose your data. So perhaps it is better not to implement the finalizer, but to always allow data loss if you forget to call Dispose () , as in this case the problem will be easier to detect.

  6. It must be remembered that the finalizer is called only once and if you resurrect an object in the finalizer by assigning a reference to it to another living object, then perhaps you should register it for finalization again using the GC method. ReRegisterForFinalize () .

  7. You can run into problems with multi-threaded applications, such as race conditions, even if your application is single-threaded. The case is absolutely exotic, but theoretically possible. Suppose there is a finalizer in your object, and another object holds a reference to it, which also has a finalizer. If both objects become available to the garbage collector, and their finalizers begin to execute and another object is resurrected, then it and your object become alive again. Now it is possible that the method of your object will be called from the main thread and simultaneously from the finalizer, since it still remains in the queue of objects that are ready for finalization. The code that reproduces this example is shown below. You can see how the finalizer of the Root object is executed first, then the finalizer of the Nested object, and after that the DoSomeWork () method is called from two threads at once.

Sample code
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 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 are called in a different order, try swapping the creation of nested and root .

findings


Finalizers in .NET is the place where the easiest way to shoot yourself in the foot. Before rushing to add finalizers for all classes that implement IDisposable , you should think about whether they really are so necessary. It should be noted that the CLR developers themselves caution against using them on the Dispose Pattern page: “Avoid making types finalizable. Carefully consider what you need. There is a real cost associated with a standpoint. ”

But if you decide to use finalizers, PVS-Studio can help you find potential errors. We have a V3100 diagnostic that will show all the places in the finalizer where NullReferenceException can occur.

Source: https://habr.com/ru/post/311998/


All Articles