7
votes

Destructors are weird. I was attempting to eliminate the need of using the disposable pattern by using 'smart' reference management, ensuring that the garbage collector could collect objects at the correct time. In one of my destructors I had to wait for an event from another object, which I noticed it didn't. The application simply shut down and the destructor was terminated in middle of execution. I'd expect a destructor is always allowed to finish running, but as the following test indicates that is not true.

using System;
using System.Diagnostics;
using System.Threading;


namespace DestructorTest
{
    class Program
    {
        static void Main( string[] args )
        {
            new DestructorTest();
            new LoopDestructorTest();
            using ( new DisposableTest() ) { }
        }
    }

    class DestructorTest
    {
        ~DestructorTest()
        {
            // This isn't allowed to finish.
            Thread.Sleep( 10000 );
        }       
    }

    class LoopDestructorTest
    {
        ~LoopDestructorTest()
        {           
            int cur = 0;
            for ( int i = 0; i < int.MaxValue; ++i )
            {
                cur = i;
            }
            // This isn't allowed to finish.
            Debug.WriteLine( cur );
        }
    }

    class DisposableTest : IDisposable
    {
        public void Dispose()
        {
            // This of course, is allowed to finish.
            Thread.Sleep( 10000 );
        }
    }
}

So, aren't destructors guaranteed to finish running?

4
Waiting for an event from another object within a finalizer? This is madness!MattDavey
You really should stick with the IDisposable pattern to have reliable destruction and resource release.Daniel Hilgarth
@MattDavey ... and waiting for an event from another object to continue triggering Dispose() isn't madness? What's the difference?Steven Jeuris
link Read the second bullet under The Finalize method might not run to completion or might not run at all in the following exceptional circumstances:Tung

4 Answers

16
votes

So, aren't destructors guaranteed to finish running?

No. From what I remember, when the process terminates it gives finalizers a couple of seconds to execute, but then terminates the process abruptly. You wouldn't want a bad finalizer to prevent a process from ever finishing, would you?

You should regard finalization as a "best effort" clean-up - in particular, it's not going to happen in situations where the whole system is abruptly shut down, such as BSOD or power outage.

EDIT: I've found some pseudo-documentation in the form of a blog post from Joe Duffy:

If a lock was orphaned in the process of stopping all running threads, then, the shutdown code path will fail to acquire the lock. If these acquisitions are done with non-timeout (or long timeout) acquires, a hang will ensue. To cope with this (and any other sort of hang that might happen), the CLR annoints a watchdog thread to keep an eye on the finalizer thread. Although configurable, by default the CLR will let finalizers run for 2 seconds before becoming impatient; if this timeout is exceeded, the finalizer thread is stopped, and shutdown continues without draining the rest of the finalizer queue.

1
votes

So, aren't destructors guaranteed to finish running?

Although its not in your code there can be instances where a GC.SuppressFinalize is explicitly called from Dispose() method. This supresses the finalization, and is for objects that does not require it.

This can improve performance significantly, as finalizable objects will always survive one garbage collection, i.e. it will be promoted to a gen1 or even a gen2 GC, which has a greater cost associated with it.

1
votes

.NET doesn't come with destructors. Your code contains finalizers instead.

Finalizers are called when the object is garbage collected, not when it is nullified. They also get limited execution time to prevent hanging objects.

See also https://en.wikipedia.org/wiki/Finalizer.

0
votes

Another way to look at is is that the finalizers are called when the garbage collector frees up memory.

However if you had a program that at most required 1Mb of memory but was running on a machine with 10Mb of memory then a valid implementation of a grabage collector would be to do nothing (as there would be enough memory for the program to execute). In that case no finalizers would ever be called.