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
- The last foreground thread of an application ends. The thread which entered the
Main
function is usually the only foreground thread in your application.
- If you run a Windows Forms application, you can call
System.Windows.Forms.Application.Exit()
to cause Application.Run
to return.
- When you call
System.Environment.Exit(nn)
.
- Pressing Ctrl-C or Ctrl-Break inside a Console application.
- Call
System.Environment.FailFast
(new in .NET 2.0) to bail out in case of fatal execution errors.
- An unhanded exception in any thread regardless if it is managed or unmanaged.
- Calls to exit/abort calls in unmanaged code.
- 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.
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");
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;
Console.WriteLine("Ctrl-C catched and " +
"translated into cooperative shutdown");
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()
{
new Thread(delegate()
{ Console.WriteLine("New Thread started");
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);
Thread t = new Thread(delegate()
{ > Console.WriteLine("Asynchronous shutdown started");
Environment.Exit(1);
});
t.Start();
t.Join();
}
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.