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
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!
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.
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
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.
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:
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:
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:
void thrd_ThreadException(SafeThread thrd, Exception ex)
{
}
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.
void thrd_ThreadCompleted(SafeThread thrd, bool hadException, Exception ex)
{
if (hadException)
{
}
else
{
}
}
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.
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!
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
.
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:
<runtime>
<legacyUnhandledExceptionPolicy enabled="1"/>
</runtime>
See MSDN for details.
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.
- 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 SafeThread
s from the threads collection