Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Managed Application Shutdown

0.00/5 (No votes)
3 Nov 2006 1  
What happens during the shutdown of a managed application?

Introduction

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 you have a chance to run some finalizers and shutdown code? As always in life, it depends. Let's have a look at the reasons why program execution can be terminated.

Reasons 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 a 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. Calls to exit/abort calls in unmanaged code.
  8. Task Manager -> End Process
What exactly happens during shutdown is explained by an excellent article by 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 finalizer 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 caught, 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 takes place.

Cooperative 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 the 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 therefore perform 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. The last mechanism I know of to shut down your process is the Task Manager which makes sure that your process goes straight to the bit nirvana.

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 is fired, you will never see a ProcessExit event because your AppDomain is already gone. These two events are mutually exclusive.
  • 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 plug-in architecture can cause unexpected side effects if the loaded modules register this handler and expect it to be called. It should be a best practice to check via AppDomain.CurrentDomain.IsDefaultDomain if your handler will ever be called before adding your handler. When your component relies on this event, you should throw, e.g., NotSupportedException. 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 of your App.config file.

    App.Config:

    <runtime>
        <legacyUnhandledExceptionPolicy enabled="1"/>
    <runtime>

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). The main problem here is if you catch the Ctrl-C/Break event and exit the handler there are no finalizers called. This is not what I would call a cooperative shutdown. The first thing that comes to my mind is to call Environment.Exit but it does not trigger any finalizers. All is not lost. I did come up with a dirty trick to run all finalizers: we 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 is 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");
             // Environment.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. Let's 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 is that there seems no time limit to 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.

Points of Interest

The provided source code shows the provided guidance here. To test one or the other scenario, you can un/comment the following test functions inside the Main method:
  • Environment.Exit - All finalizers are called.
  • CriticalFinalizers - Critical/Normal finalization is aborted if an exception occurs during finalization.
  • CreateAndExecuteAppDomain - Check if certain events are called inside another AppDomain.
  • RudeThreadExitExample - Forces an UnhandledException event.
More information about many other things can be found at my blog.

History

  • 30.10.2006 - First release on CodeProject.
  • 2.11.2006 - Updated source code with the correctly commented source code.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here