Article 991K When everything you know is wrong, part one

When everything you know is wrong, part one

by
ericlippert
from Fabulous adventures in coding on (#991K)

Finalizers are interesting and dangerous because they are an environment in which everything you know is wrong. I've written a lot about the perils of C# finalizers / destructors (either name is fine) over the years, but it's scattered in little bits over the internet. In this series I'm going to try to get everything in one place; here are a bunch of things that many people believe about finalizers, all of which are wrong.

First off, since this is a blog and not a suspense novel, let me briefly describe "normal" - and I use the term guardedly - finalization semantics in the CLR.

  • The GC runs (on the GC thread) and identifies that an object has no more living references.
  • If the object has a finalizer and is a candidate for finalization, it goes on the finalization queue instead of being reclaimed.
  • The finalizer thread calls finalizers on objects on the queue, removes them from the queue, and marks them as "not candidates for finalization".
  • The GC runs again; the objects that were dead before are still dead, but this time they are no longer candidates for finalization, so their memory is reclaimed.

That brief sketch glosses over many interesting corner cases, some of which we'll look at in this series. This series should not be considered a complete list of everything that can go wrong with finalizers. If you want a deeper, more accurate, and historically interesting look at how finalizers were designed in the early days of .NET, see cbrumme's seminal 2004 post on the subject.

In this episode we'll examine false beliefs that people have about when a finalizer is required to run.

Myth: Setting a variable to null causes the finalizer to run on the object that was previously referenced by the variable.

foo = null; // Force finalizer to run

Setting a variable to null does not cause anything to happen immediately except changing the value of the variable. If the variable was the last living reference to the object in question then that fact will be discovered when the garbage collector runs the collector for whatever generation the object was in. (If it runs at all, which it might not. There is no guarantee that the GC runs.)

Myth: Calling Dispose() causes the finalizer to run.

foo.Dispose(); // Force finalizer to run

Dispose() is nothing special to the runtime; the convention that we call a method named Dispose() when an object has resources to clean up is not encoded into the runtime. It's just a method.

The standard pattern for implementing Dispose() on an object with a finalizer is: both the Dispose() method and the finalizer should call a method Dispose(bool), where the flag is true if the caller is Dispose() and false if it is the finalizer. The Dispose(bool) method then does the work of both the Dispose() method and the finalizer, as appropriate.

Myth: Calling GC.Collect() causes finalizers to run.

No, it causes a collection to happen. That might identify objects that are candidates for finalization, but it does not force the finalizer thread to be scheduled. If that's what you want - for testing purposes only please! - then call GC.WaitForPendingFinalizers().

Myth: Finalizers are guaranteed to run for every finalizable object.

The SuppressFinalize method causes finalization to be suppressed, hence its name. So there is no guarantee that a given object will be finalized; its finalization could be suppressed. (And in fact it is common for Dispose() to suppress finalization, so that the object's memory can be reclaimed earlier. Note that in the sketch above, a finalizable object needs to be detected by the GC twice before its memory is reclaimed. We'd like objects where Dispose() was called to get reclaimed after only one collection.)

Myth: Finalizers are guaranteed to run eventually for every finalizable object where finalization has not been suppressed.

Suppose there is an object with a finalizer that is allocated; a reference to the object is placed into a static field, and the program then goes into an infinite loop. Objects referenced by static fields are alive, and therefore the garbage collector never detects that the object is dead, and therefore never puts it on the finalizer queue.

Myth: Finalizers are guaranteed to run eventually if there are no more living references to an object.

The garbage collector places objects that are no longer referenced onto the finalizer queue, but that requires that the garbage collector run. The last reference to a finalizable object could be in a local variable, and when the method returns, the object is no longer referenced. The program could then go into an infinite loop which allocates no memory, and therefore produces no collection pressure, and therefore the garbage collector never runs.

Myth: Finalizers are guaranteed to run at some time after the garbage collector has determined that an object is unreachable.

In that case the garbage collector will put a reference to the object onto the finalization queue. But the finalizer thread is a separate thread, and it might never be scheduled again. There could always be a higher-priority thread.

Myth: Finalizers of objects awaiting finalization are guaranteed to run provided that the finalizer thread has been scheduled to run.

There is no guarantee that the finalizer thread will get to all the objects in the finalization queue. A number of situations, such as a finalizer taking too long, a finalizer throwing an exception, a finalizer deadlocking, an AppDomain being unloaded, the process failing fast or someone simply pulling the power cord out of the wall can cause finalization to never happen for an object in the finalizer queue.

Myth: Finalizers of objects awaiting finalization are guaranteed to run provided that the GC runs, and the GC identifies a finalizable object, and the finalizer thread has been scheduled to run, and the finalizer is fast, and the finalizer does not throw or deadlock, and no one pulled the power cord out of the wall.

Nope! Here we produce collection pressure, so the GC is running, we are identifying finalizable objects, and clearly the finalizer thread is running because we get output. Care to predict the behavior of this program?

class Weird{ static int count = 0; ~Weird() { count += 1; System.Console.WriteLine(count); new Weird(); } static void Main() { new Weird(); }}

The program terminates without error after having created just over 14000 objects on my machine. That means that one of those objects was never finalized.

What is going on here? When finalizers are being run because a process is being shut down, the runtime sets a limit on how much time the finalizer thread gets to spend making a good-faith effort to run all the finalizers. If that limit is exceeded then the runtime simply stops running more finalizers and shuts down the program. (Again, a good historical overview of how this was designed can be found here. I don't have a more up-to-date exegesis handy.)

All of the myths so far imply that if you have code that absolutely, positively must run because it has some important real-world impact then a finalizer is not your best choice. Finalizers are not guaranteed to run.

Next time on FAIC: Even more myths about finalizers.


2421 b.gif?host=ericlippert.com&blog=67759120
External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments