Search
Close this search box.

Cooperative Application Shutdown with the CLR

All good things have to end even your perfectly working managed executable. But do you know in what circumstances the CLR will terminate your program and much more importantly when do you have a chance to run some finalizers and shutdown code? As always in live it depends. Lets have a look at the reasons why program execution can be terminated.

Ways to terminate an application:

  1. The last  foreground thread of an application ends. The thread which entered the Main function is usually the only  foreground thread in your application.
  2. If you run an Windows Forms application you can call System.Windows.Forms.Application.Exit() to cause Application.Run to return.
  3. When you call System.Environment.Exit(nn)
  4. Pressing Ctrl-C or Ctrl-Break inside a Console application.
  5. Call System.Environment.FailFast (new in .NET 2.0) to bail out in case of fatal execution errors.
  6. An unhanded exception in any thread regardless if it is managed or unmanaged.
  7. Unmanaged exit calls within unmanaged code.

What exactly happens during shutdown is explained by an excellent article of Chris Brumme.

Shutdown Process
Generally speaking we can distinguish between two shutdown types: Cooperative and Abnormal. Cooperative means that you get some help from the CLR (execution guarantees, callback handlers, …) while the other form of exit is very rude and you will get no help from the runtime.
During a cooperative shutdown the CLR will unload the Application Domain (e.g. after leaving the Main function). This involves killing all threads by doing a hard kill as opposed to the “normal” ThreadAbortException way where the finally blocks of each thread are executed. In reality the threads to be killed are suspended and never resumed. After all threads sleep the pending finalizers are executed inside the Finalizer thread. Now comes a new type of finalizers into the game which was introduced with .NET 2.0. The Critical Finalizers are guaranteed to be called even when normal finalization did timeout (for .NET 2.0 the time is 40s ) and further finalizer processing is aborted. If you trigger an exception inside a finalizer and it is not catched you will stop any further finalization processing, including the new Critical Finalizers. This behavior should be changed in a future version of the CLR. What are these “safe” finalizers good for when an “unsafe” finalizer can prevent them from running? There is already a separate critical finalizer queue inside the CLR which is processed after the normal finalization process did take place.

Normal Shutdown
A fully cooperative shutdown is performed in case of 1, 2 and 3. All managed threads are terminated without further notice but all finalizers are executed. No further notice means that no finally blocks are executed while terminating the thread.

Abnormal Shutdown
The bets are off and no CLR event is fired which could trigger a cleanup action such as calling finalizer, or some events. Case 4 can be converted into a normal shutdown by the code shown below. Case 6 can be partially handled inside the AppDomain.UnhandledException. The other ones (Environment.FailFast and unmanaged exits) have no knowledge of the CLR and perform therefore a rude application exit. The only exception is the Visual C++ (version 7+) Runtime which  is aware of the managed world and calls back into the CLR (CorExitProcess) when you call the unmanaged exit() or abort() functions.

Interesting Events
All these events are only processed when we are in a cooperative shutdown scenario. Killing your process via the Task Manager will cause a rude abort where none of our managed shutdown code is executed.

  • The AppDomain.DomainUnload  event is not called in your default AppDomain. When you exit e.g. the Main function the default AppDomain is unloaded but your event handler will never be called. The DomainUnload handler is called from an arbitrary thread but always in the context of the domain that is about to be unloaded.
  • AppDomain.ProcessExit is called when the CLR does detect that a shutdown has to be performed. You can register in each AppDomain such a handler but be reminded that after the DomainUnload event was fired you will never see a ProcessExit event because your AppDomain is already gone. These two events are mutually exclusive. When you kill the process via 
  • AppDomain.UnhandledException is called only in the default AppDomain. You can register this handler only in the “root” AppDomain which is the first one created for you by the CLR. Hosting of other AppDomains in a plugin architecture can cause unexpected side effects if the loaded modules register this handler and expect it to be called. When the handler is left the process finally terminates which cannot be prevented in .NET 2.0 except if you set a compatibility flag inside the runtime settings your App.config file. <legacyUnhandledExceptionPolicy enabled=”1″/>

Ctrl-C/Ctrl-Break

The default behavior of the CLR is to do nothing. This does mean that the CLR is notified very late by a DLL_PROCESS_DETACH notification in which context no managed code can run anymore because the OS loader lock is already taken. We are left in the unfortunate situation that we get no notification events nor are any finalizers run. All threads are silently killed without a chance to execute their catch/finally blocks to do an orderly shutdown. I said in the first sentence default because there is a way to handle this situation gracefully. The Console class has got a new event member with .NET 2.0: Console.CancelKeyPress. it allows you to get notified of Ctrl-C and Ctrl-Break keys where you can stop the shutdown (only for Ctrl-C but not Ctrl-Break). If you want to do a coordinated shutdown inside the handler you will run into the gotcha that calling Environment.Exit does not trigger any finalizers. When we do have other plans to exit the process we need to spin up a little helper thread inside the event handler which will then call Environment.Exit. Voila our finalizers are called.

Graceful Shutdown when Ctrl-C or Ctrl-Break was pressed.

  static void GraceFullCtrlC()

        {

            Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e)

            {

                if( e.SpecialKey == ConsoleSpecialKey.ControlBreak )

                {

                    Console.WriteLine("Ctrl-Break catched and translated into an cooperative shutdown");

                    // Envirionment.Exit(1) would NOT do a cooperative shutdown. No finalizers are called!

                    Thread t = new Thread(delegate()

                                    {

                                        Console.WriteLine("Asynchronous shutdown started");

                                        Environment.Exit(1);

                                    });

                    t.Start();

                    t.Join();

                }

                if( e.SpecialKey == ConsoleSpecialKey.ControlC )

                {

                    e.Cancel = true; // tell the CLR to keep running

                    Console.WriteLine("Ctrl-C catched and translated into cooperative shutdown");

                    // If we want to call exit triggered from out event handler we have to spin

                    // up another thread. If somebody of the CLR team reads this. Please fix!

                    new Thread(delegate()

                    {

                        Console.WriteLine("Asynchronous shutdown started");

                        Environment.Exit(2);

                    }).Start();

                }

            };

            Console.WriteLine("Press now Ctrl-C or Ctrl-Break");

            Thread.Sleep(10000);

        }

Unhandled Exceptions

It is very easy to screw up your application with an abnormal application exit. Lets suppose the following code:

  static void RudeThreadExitExample()

        {

            // Create a new thread

            new Thread(delegate()

            {

                Console.WriteLine("New Thread started");

                // throw an unhandled exception in this thread which will cause the application to terminate

                // without calling finalizers and any other code.

                throw new Exception("Uups this thread is going to die now");

            }).Start();

        }

We spin up an additional thread and create an unhandled exception. It is possible to register the AppDomain.Unhandled exception handler where you can e.g. log the exception but nonetheless your application will be rudely terminated. No finalizers are executed when an unhandled exception occurs. If you try to fix this by calling Environment.Exit inside your handler nothing will happen except that the ProcessExit event is triggered. We can employ our little Ctrl-C trick here and force an ordered cooperative shutdown from another thread.

Cooperative Unhandled Exception Handler (Finalizers are called)

 static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)

        {

            Console.WriteLine("Unhandled exception handler fired. Object: " + e.ExceptionObject);

            // Prepare cooperative async shutdown from another thread

            Thread t = new Thread(delegate()

                                {

                                    Console.WriteLine("Asynchronous shutdown started");

                                    Environment.Exit(1);

                                });

            t.Start();

            t.Join(); // wait until we have exited

        }

This way you can ensure that your application exits in a much cleaner way where all finalizers are called. What I found interesting during my investigation that there seems no time limit to exist how long I want to run the unhandled exception handler. It is therefore possible that you have several unhandled exception handlers running at the same time. An ordered application shutdown can be quite twisted if you want to do it right. Armed with the knowledge from this post you should have a much better feeling what will happen when things go wrong now

This article is part of the GWB Archives. Original Author: Alois Kraus

Related Posts