Stop using Finalizers in C#

15/05/2022
FinalizerC#GC

💡 Disclaimer: As almost all things in this blog this boils down to my experience and personal preferences. So take it with a grain of salt. I would love to hear your opinion about that.


For starters let's look what a finalizer is and how it differs from the IDisposable or IDisposableAsync interface we also have in .NET,

Finalizers

If you come from a C/C++ world you can think of Finalizers as kind of a destructor. They are a way of cleanup resources when your class gets removed. But there are big differences and Finalizer != Destructor. First things first C/C++ is not a managed language with garbage collection. Second and most important you can not directly control when a Finalizer is called in C#.

Defining a finalizer

We can define a finalizer like this:

public void MyClass
{
    ~MyClass() { }
}

Usage of finalizers

Now that we know how to use them there is a central question in the room: When do we use finalizers? and also When to use a Finalizer and when to use IDisposable?

I'll try to answer both question with answer the latter one. IDisposable is also meant for cleaning up unmanaged resources. So what is the difference? In simple terms: A finalizer is a convenient way to dispose resources if your user didn't call Dispose. Why? Because in contrast to Dispose the finalizer will be called one way or another (not always as we see later on). But that leads (in my opinion) to a problem: You are trying to be extremely defensive for your users. Stop doing that! Give your user the possibility to dispose resources deterministically via IDisposable/IDisposeableAsync. If he does not call that, it is not your duty to do so.

I also see a lot of use cases where people in their finalizers just call Dispose of their dependent resources like the following (simplified):

public class MyClass
{
    private Stream stream;

    ~MyClass() => stream.Dispose();
}

Again. That is very nice that you clean up after your user, but you shouldn't do that. If you have indirect unmanaged resources you should rely on that your reference will cleanup itself when it is important.

So what would be the right way here in my opinion? Use IDisposable:

public class MyClass : IDisposable
{
    private Stream stream;

    public void Dispose() => stream.Dispose();
}

Your part is done and the user is responsible for calling Dispose or wrapping it in a using statement.

Finalizers and the GC

You could argue until now what I am telling are theoretical problems, which to some degree I would agree. So let's face some real consequences with the finalizer.

The way finalizers and the GC works is as follows:

The GC checks for dead references and what it can clean up (Mark phase). Now it detects our class which in theory can be disposed. But our class has a finalizer, so the GC can not directly remove it from the heap. Instead it puts it into a finalizer queue, where then our finalizer method is called and afterwards it gets removed (Sweep phase). So instead of Gen 0 our object gets removed in Gen 1. It lives longer than it should and puts a little bit more pressure on the GC. And this applies to all references our class with a finalizer has too. That means they also can not get directly garbage collected and have to wait until our class is cleaned up by the GC.

I will demonstrate that with a small example. And the example itself is a very big anti-pattern: Do not use empty Finalizers. They make everything worse:

[MemoryDiagnoser]
public class EmptyFinalizer
{
    [Benchmark(Baseline = true)]
    public ClassWithoutFinalizer WithoutFinalizer() => new(); 
    
    [Benchmark]
    public ClassWithEmptyFianlizer WithEmptyFinalizer() => new();
}

public class ClassWithEmptyFianlizer
{
    ~ClassWithEmptyFianlizer() { }
}

public class ClassWithoutFinalizer
{
}

The results:

|             Method |       Mean |     Error |    StdDev | Ratio | RatioSD |  Gen 0 |  Gen 1 | Allocated |
|------------------- |-----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|
|   WithoutFinalizer |   2.205 ns | 0.1230 ns | 0.1416 ns |  1.00 |    0.00 | 0.0057 |      - |      24 B |
| WithEmptyFinalizer | 144.038 ns | 2.9594 ns | 6.1773 ns | 65.32 |    4.19 | 0.0057 | 0.0029 |      24 B |

We see that our class without finalizer gets removed in Gen 0, but our class with an empty finalizer lives longer and it is quite expensive to get it cleaned up. Again I want to stress out that this holds true for the whole dependency graph of our object.

Other problems

Still not convinced? Well let's continue our journey: "How to shoot yourself in the foot easily!"

Exceptions

If there is an exception inside your finalizer your application will crash. Then can easily happen if you have IDisposable and a finalizer and you are not careful enough that some resources are already disposed and throw an exception. Or objects are null and you are still accessing them. If you have an IDisposable that is simple. Wrap it in a try-catch block. It can happen that the finalizer is not called at all. If another finalizers throws, our application crashes and our finalizer gets never called.

Multithreading

The GC runs in its separate thread (besides things like Blazor WASM where you only have one thread overall). That means even if you have a single thread application you might have to take care of race conditions.

Not deterministic

As discussed earlier you don't know when your objects get put into the finalizer queue and afterwards sweeped away. Sure you can do this:

GC.Collect();
GC.WaitForPendingFinalizers();

Which itself is almost an antipattern. There are rare use cases where you need such things. One would be game development where extensive resources are loaded and before you start the game you want to remove the GC pressure. Here it doesn't matter if the user waits another second because it was anyway a loading screen.

But if you don't do this you do not know when your finalizer is called. Maybe not so great if you want to close a database connection or close other native handles.

Async resources

There is a reason we have IAsyncDisposable and disposing async stuff in the finalizer can get tricky for a whole lot reasons. Not to mention that ValueTask has some pitfalls on its own. Like multiple awaiting can lead to undefined results. Here are some pitfalls with ValueTask which would apply in this scenario as well.

Alternatives

Even in cases where a finalizer makes sense because you have a native OS handle, there are alternatives which are safer. Here is a very good article about that. The article highlights how to use SafeHandle with IDisposable instead of a finalizer.

Usage of finalizers - Part 2

If you still need a finalizer, especially in combination with the IDisposable interface, I'd recommend such a structure:

public class FinalizerClassTemplate : IDisposable
{
    ~FinalizerClassTemplate()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // dispose managed resources
        }
    }
}

0.1% use case

Of course there are some valid use cases for a finalizer. Most of them are very rare and super specialized. For example:

  • If you really have to cleanup large resources where the impact on your application would be crucial if not done. For example large buffers rented from an ArrayPool should be returned, but even this should be measured first. Because even the ArrayPool can rehydrate itself in case of starvation. So the buffer has to be quite big to justify a finalizer.
  • Some COM interfaces need proper cleanup in a certain way.
  • When calling GC.AddMemoryPressure one should call GC.RemoveMemoryPressure. That is especially vital when dealing with unmanaged resources.

You see these are really corner cases which in theory can all be dealt with IDisposable but having the safety net can be justified.

Conclusion

Finalizers are tricky and in 99.9% of the cases IDisposable or IDisposableAsync is the better choice. It is safer, predictable and has better performance. Try to avoid them or use safer options.

Resources

  • The benchmark and the template class can be found here
  • All my examples in my blog can be found here
6
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x