Finalizers And The Resurrection Pattern In C#

Posted by : on

Category : C#

Introduction

When we create objects from a type, these objects require memory to exist, as they contain data that they use. These objects, or instances, often have a method called a constructor, which is responsible for their initialization. Initialization refers to the values we want the object’s variables to have when it is first created. This is achieved through the constructor. The constructor is a method that runs only once and always before any other method, when an instance is first created.

The counterpart to the constructor is the destructor, or finalizer, as it is called in C#. The finalizer runs before an object is released from memory. Since C# is a managed language, meaning that the programmer is not responsible for manually releasing an object’s memory—this is automatically handled by a mechanism called the garbage collector—finalizers have limited use.

Due to the automatic memory management provided by the garbage collector, there are certain nuances to how finalizers should be used. In this post, we will explore how to use finalizers, when an object becomes eligible for garbage collection, potential pitfalls in writing finalizer code due to automatic memory management, how finalizers can be run more than once, and finally, how we can use finalizers to make an object that is eligible for garbage collection available for use again by our code. This practice is known as the resurrection pattern, and we will discuss why and when it should be used.

Root Objects

Let’s begin by understanding how the GC (Garbage Collector) determines that an object is eligible for garbage collection. At certain intervals, the GC initiates a garbage collection. The exact timing of this process is influenced by several factors, such as the amount of available memory and the time since the last collection. However, the focus here is not on when this happens but on how the GC decides which objects can be collected. It starts by examining root object references and traverses their references in an object graph, marking all objects referenced by the root objects, as well as the objects referenced by those objects, and so on. Ultimately, any objects that have not been marked are deemed unused and are therefore eligible for garbage collection.

Any object that is not directly or indirectly referenced by a root object is considered eligible for garbage collection. Root objects fall into three main categories:

  1. A variable that exists in the stack or has its reference stored there, such as local variables and method parameters.
  2. A static variable.
  3. An object in the finalization queue (an object that has a finalizer and is eligible for garbage collection but whose finalizer has not yet run).

For this post, the third case is the most relevant to our discussion.

C# Finalizers

A finalizer is declared similarly to a constructor but with a few key differences:

  1. The finalizer has a prefix: The ~ symbol.
  2. Finalizers cannot be defined in structs; they are used only in classes.
  3. A finalizer cannot have any modifiers or parameters (therefore each class has only one finalizer)
  4. A finalizer cannot be inherited.
  5. A finalizer cannot be called directly.

Although a finalizer cannot be inherited, each finalizer implicitly calls the finalizer of its base class if the object inherits from a class. Additionally, since the finalizer cannot be called directly, the programmer has no control over when it will run—the garbage collector is solely responsible for calling the finalizer.

Here’s an example of a finalizer:

class Example
{
   ~Example()
   {
      // finalization here
   }
}

When an object becomes eligible for garbage collection, one of two things can happen: if the object does not have a finalizer, it is deleted immediately. However, if it has a finalizer, it is placed in a special queue. This queue holds objects with finalizers, and the garbage collector is responsible for executing the finalizers before deleting the objects. This process occurs on a separate thread (or threads) that run in parallel with our program.

Due to this procedure, having finalizers in our types can negatively impact program performance:

  • Memory release is delayed because the GC has to track finalizers that have not yet run.
  • Objects with finalizers are considered root objects, meaning any references they hold to other objects keep those objects alive. This leads to wasted memory, as those objects cannot be collected until the finalizer has finished executing.
  • The programmer cannot predict when or how objects enter the finalization queue, making it impossible to determine the order in which finalizers will run.
  • A special case occurs when a finalizer needs to wait for something to happen. This can block the execution of other finalizers in the queue, wasting processing power and memory.

Generally finalizers should execute quickly without waiting for something to happen, and should not reference other objects with finalizers. Additionally, finalizers should not throw exceptions, as these exceptions cannot be caught and would crash the program. Even if we are careful with our finalizers, at minimum we double the job the GC has to do with traversing the object graph and discovering objects eligible for garbage collection, because instead of releasing the object with the finalizer and all its direct and indirect references, it has to traverse that part again.

Another reason to be cautious with finalizers is that they may run if an object throws an exception during construction. If the exception is caught, the object may not have been properly initialized, so a finalizer should never assume that the object’s fields have been initialized correctly.

Finally, finalizers may or may not run if an application fails to terminate cleanly. Whether finalizers are part of an application’s shutdown process depends on the .NET implementation. For example, .NET Framework attempts to execute finalizers for objects that haven’t been garbage collected when the program terminates, whereas .NET does not call finalizers during application shutdown.

Given all this, finalizers should be used sparingly. Their most common use case is to release unmanaged resources that the object is using. While it is generally the responsibility of the programmer to release these resources via the Dispose method, not all programmers will remember to call Dispose. Therefore, the finalizer should also call Dispose, even though this process will be slower than manually disposing of the resources.

The Resurrection Pattern

The resurrection pattern is a technique used to prevent garbage collection for an object that is eligible for it.

This is an edge case where we need to release a resource in a finalizer, but the operation fails. If the operation throws an exception, it would cause the program to crash. While we can wrap the operation in a try/catch block, this presents a dilemma: either we “swallow” the exception with an empty catch block and remain unaware that something went wrong during the resource release, or we attempt to fix the error within the catch block. However, error handling takes processing power and time, which could block all the objects in the finalization queue and keep their explicitly and implicitly referenced objects alive. This would put extra pressure on the garbage collector, as it would need to keep checking these objects while the finalizer resolves the error.

One way to handle this situation is to have the finalizer rerun multiple times to retry the operation. This would reinsert the object into the finalization queue, allowing other finalizers to run. Alternatively, the resurrection pattern can be used: in the finalizer, we create a reference to the object in a live root object, allowing us to manually handle the issue.

These two approaches are not mutually exclusive—they can be combined. We can try rerunning the finalizer a few times, and if those attempts fail, we can resurrect the object. Let’s see those two solutions in code:

Re-Running A Finalizer

Let’s suppose we have the following class, that represents an unmanaged resource that we can release and it throws.

static class Resource
{
   public static void Release(bool isThrowing)
   {
      if (isThrowing) throw new Exception("Error!");
   }
}

For simplicity, we won’t implement the Disposable pattern, we will try to release it directly from the finalizer. If we want our finalizer, to try a number of times to release the resource if there is an error, then we should write our code like this:

public class ReRunExample
{
   private readonly bool _shouldThrow;
   private int _noOfTries;

   public ReRunExample(bool shouldThrow) => _shouldThrow = shouldThrow;

   ~ReRunExample()
   {
      try
      {
         Resource.Release(_shouldThrow);
      }
      catch
      {
         if (_noOfTries++ < 3) GC.ReRegisterForFinalize (this);
         // Don't do this logging in a real world scenario, this is for demonstration purposes only.
         Console.WriteLine($"Finalizer run {_noOfTries} times!");  
      }
   }
}

This uses the GC.ReRegisterForFinalize method to allow the garbage collector to run the finalizer more than one time. You should be careful with this method, if you call it twice, then you will have three finalizations! (The original plus two more).

Although this method allows us to retry if the release fails, it is not practical for manually fixing the problem. As mentioned earlier, running expensive or time-consuming code in a finalizer is not advisable. Therefore, if we need to manually resolve the issue, we will have to use object resurrection.

Resurrecting Our Object

The simplest way to resurrect an object—by creating a reference to this in a root object—is to assign it to a static variable.

Since there may be many objects, it’s preferable for this variable to be a collection. This collection should be thread-safe, as multiple threads may be executing finalizers simultaneously. Therefore, adding objects to the collection must be done in a thread-safe manner. Additionally, when removing one object from the collection, another object could be added by its finalizer running on a different thread, so a thread-safe collection is essential.

Here’s an example that is using a ConcurrentQueue:

public static class FailedFinalizers
{
   public static readonly System.Collections.Concurrent.ConcurrentQueue<ResurrectionExample> FailedExamples = new();
}

public class ResurrectionExample
{
   private readonly bool _shouldThrow;
   public Exception? Error { get; private set; }

   public ResurrectionExample(bool shouldThrow) => _shouldThrow = shouldThrow;

   ~ResurrectionExample()
   {
      try
      {
         Resource.Release(_shouldThrow);
      }
      catch (Exception ex)
      {
         Error = ex;
         FailedFinalizers.FailedExamples.Enqueue (this); 
      }
   }
}

Now our object is being referenced from the FailedExamples queue, so it has been “resurrected”. It is not eligible for garbage collection anymore and now we have the opportunity to deal with whatever problem happened manually. We can have a mechanism that checks the queue and if not empty dequeue the objects, find the exception that was caused with the Error property and try to deal with it.

Conclusion

This was an edge case in the use of finalizers. Finalizers are rarely needed, as they are primarily useful for releasing unmanaged resources, and resurrection is even rarer. If you want to try these examples yourself, be cautious—finalizers are not guaranteed to run. Eric Lippert has two excellent blog posts about when finalizers are required to run and when are required not to run: When everything you know is wrong, part one and When everything you know is wrong, part two.

If you want to try the examples then you have to write code like this:

for (int i = 0; i < 2; i++)
{
   var example = new ReRunExample(true);
}

for (int i = 0; i < 10; i++)
{
   GC.Collect();
   GC.WaitForPendingFinalizers();
}

The GC.Collect forces the GC to check for objects that are eligible for garbage collection and the GC.WaitForPendingFinalizers causes the finalizers to run. Also declaring a variable and then making it null, won’t force the GC, so the for loop is needed.

Use these for testing purposes only!!!

The result of the above would be:

Finalizer run 1 times!
Finalizer run 2 times!
Finalizer run 3 times!
Finalizer run 4 times!

Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.


Follow me: