Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

SafeThread - Prevent Unhandled Exceptions in Secondary Threads

4.76/5 (33 votes)
19 Aug 2008CPOL6 min read 1   2.3K  
Prevent unhandled exceptions in secondary threads, using SafeThread
Image 1

Introduction

Unhandled exceptions can occur in almost any program. When they happen in a secondary (worker) thread, they can kill the application - or worse, be ignored! Prevent unhandled exceptions in secondary threads by using SafeThread.

Contents

Background

While researching the behavior of unhandled exceptions in .NET 2.0 applications for the CALM project, one of the more surprising and frustrating findings was the way in which unhandled exceptions in secondary threads tend to kill the application or be completely ignored! Unhandled exceptions in secondary threads (threads that you create explicitly) will kill the application, even if you have an unhandled exception event handler (plus the contextual information, i.e., which thread, is lost). Unhandled exceptions in worker (ThreadPool) threads and Timer threads, in particular, are ignored by the Common Language Runtime (CLR). These threads just die, with no unhandled-exception event to trap, no warning to the user, nothing. Obviously, in production applications, this is unacceptable (mis-)behavior!

SafeThread

Thus, the SafeThread class was created. SafeThread wraps a regular CLR Thread, but executes the method delegate inside a try-catch block that emits an event when an unhandled exception occurs. Developers can use this event to clean up after the thread, classify the kind of exceptions, or even launch a new thread. For certain kinds of threaded operations like "heartbeat" operations, this is valuable and necessary for a robust application.

The premise behind SafeThread is fairly simple. Since threads rely on a method delegate for execution, all we need to do is wrap the execution of the delegate in a try-catch block and emit an event if we catch an exception. The base Thread class supports a single-parameter delegate and a no-parameters delegate, so SafeThread mimics these constructors. In addition, SafeThread supports the new dynamic delegates (also known as, anonymous methods) for additional convenience. Finally, to complete the facade, SafeThread implements all of the public methods and properties of the Thread class. SafeThread also provides a ThreadCompleted event to signal when processing is completed.

SafeThread Implementation

Clearly, it would be best if SafeThread could inherit from Thread, but this is not possible since the Thread class is sealed (MustInherit in VB.NET terminology). The next-best solution would be for SafeThread to implement a common interface for threads (such as IThread), but alas, .NET does not have this either. So, the best we can do is re-implement the interface and wrap a Thread object. Note that this does present a few issues in that some properties and methods of SafeThread are not valid unless the wrapped Thread already exists, as the wrapped Thread object is not created until the Start method is called.

SafeThread Usage

SafeThread may be used exactly as the CLR Thread class, e.g., using a ThreadStart, with the addition of a ThreadException event and a flag to control whether calling Abort on the thread is reported as an exception or not.

C#
SafeThread thrd = new SafeThread(new ThreadStart(this.threadRunner));
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

In addition to ThreadStart and ParameterizedThreadStart, SafeThread also offers a SimpleDelegate option, which can be used with any void no-argument method:

C#
SafeThread thrd = new SafeThread((SimpleDelegate)this.threadRunner));
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

SimpleDelegate can also be used with an anonymous method:

C#
SafeThread thrd = new SafeThread((SimpleDelegate) 
           delegate { this.threadRunner(); });
thrd.ShouldReportThreadAbort = true;
thrd.ThreadException += 
     new ThreadThrewExceptionHandler(thrd_ThreadException);
thrd.ThreadCompleted += 
     new ThreadCompletedHandler(thrd_ThreadCompleted);
thrd.Start();

The ThreadException handler is passed the SafeThread object that threw the exception, and also the Exception that was thrown:

C#
void thrd_ThreadException(SafeThread thrd, Exception ex)
{
    //do something here, like restart the thread 
}

The ThreadComleted handler is passed the SafeThread object that completed processing, along with a bool to say whether processing was terminated due to an exception or not, and the offending Exception or null (Nothing in VB.NET terminology) if processing completed successfully.

C#
void thrd_ThreadCompleted(SafeThread thrd, bool hadException, Exception ex)
{
    if (hadException)
    {
        //the thread terminated early due to an unhandled exception in ex
    }
    else
    {
        //the thread completed successfully
    }
}

SafeThread Demo Application

The SafeThread demo application is both contrived and silly: each SafeThread created periodically pulses the animation of the Start SafeThread button in a circle. Each SafeThread created will pulse the animation 99% of the time, and 1% of the time will throw an unhandled divide-by-zero exception. If the SafeThread survives 100 iterations, it completes successfully and emits a ThreadCompleted event. The ListBox shows what is happening with each SafeThread.

To use the SafeThread demo application, build and/or run it. Click the Start SafeThread button several times - the more SafeThread objects created, the faster the button will move in a circle. Click the Stop SafeThread button to end the SafeThread objects in the order created. Click a ListBox item to see the full item text in a MessageBox. All of the SafeThread objects are ended when the application is closed.

UnsafeThread Demo Application

Just for fun, and to address some good points raised by Daniel Grunwald in the comments below, I have added an "unsafe" thread demo application to the article to better illustrate what happens without exception trapping or SafeThread. Click the Start Unsafe Thread button to launch a regular CLR Thread that throws an unhandled divide-by-zero exception, and note that with no protection at all, what you get is the send-a-debug-report-to-Microsoft dialog, and the application dies. Run the demo again, but this time, first check the Use Unhandled Exception Handlers checkbox and then click the Start Unsafe Thread button. Now, we get a message box, courtesy of the AppDomain.UnhandledException event... and then, we get the send-a-debug-report-to-Microsoft dialog, and the application dies.

This illustrates several issues with just using the System.AppDomain.CurrentDomain.UnhandledException event handler to trap exceptions in 'pure' secondary threads (i.e., not worker threads or Timer threads):

  • There is not enough contextual information available in the event handler to know which thread caused the problem; the sender argument is the application, not the thread.
  • By the time the unhandled exception reaches the event handler, it is too late to do anything about it - the application is already terminating!
  • No matter what you do, the application is going to die.
  • Finally - and this is the thing I dislike the most about this mechanism - after your unhandled exception event handler finishes, the dreaded send-a-debug-report-to-Microsoft dialog appears!

Conclusion

SafeThread has a few more properties of interest:

  • SafeThread has a Name property which is passed to the underlying CLR thread on Start. The default CLR thread Name is SafeThread#XXX where XXX is the HashCode of the CLR Thread object.
  • SafeThread remembers its start argument in the ThreadStartArg property, when used with a ParameterizedThreadStart.
  • SafeThread provides a generic Tag object property, which is useful for remembering arbitrary information about the thread.
  • SafeThread provides a LastException property, which records the last exception captured by the SafeThread.

Notes

Note that the behavior of unhandled exceptions in secondary threads changed in .NET 2.0. In .NET 1.1, an unhandled exception in a worker (ThreadPool) thread (but not a Timer thread) would be captured by the AppDomain's UnhandledException event. Microsoft provides a backwards-compatibility option in the application configuration file:

XML
<runtime>
    <legacyUnhandledExceptionPolicy enabled=&quot;1&quot;/>
</runtime>

See MSDN for details.

Future Directions

A SafeThreadPool would be a logical component to use the SafeThread class, as would a SafeTimer class. These may be covered in future article updates.

Revision History

  • 08-07-2008
    • Initial version of article published
  • 08-08-2008
    • UnsafeThread demo and source added, to show what happens without SafeThread
    • UnsafeThread demo description section added
    • Edited the Background section to clarify the different behavior of the secondary thread types
    • Edited the Notes section to clarify the purpose of the legacy-compatibility option
    • Extra License section removed
  • 08-17-2008
    • SafeThread class updated to emit the ThreadCompleted event
    • SafeThreadDemo application updated to use ParameterizedThreadStart to pass a time-to-live argument to the threadRunner method, and to hook the ThreadCompleted event and update the text in the ListBox
    • SafeThreadDemo application corrected to remove completed SafeThreads from the threads collection

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)