Making our way through our detailed .NET Exception Handling series, today we'll dig into the fun System.Threading.ThreadAbortException. A System.Threading.ThreadAbortException
is thrown when the Abort()
method is invoked on a Thread
instance.
In this article we'll go over the ThreadAbortException
in more detail, examining where it resides in the .NET exception hierarchy, along with some functional sample code illustrating how System.Threading.ThreadAbortExceptions
are commonly thrown, so let's get to it!
The Technical Rundown
- All .NET exceptions are derived classes of the
System.Exception
base class, or derived from another inherited class therein. System.SystemException
is inherited from theSystem.Exception
class.System.Threading.ThreadAbortException
inherits directly fromSystem.SystemException
.
When Should You Use It?
To understand the purpose of the System.Threading.ThreadAbortException
we first need to discuss threading in .NET and, specifically, how threads can be terminated. One simple technique of halting a thread is to call the Abort()
method on the thread in question. Doing so will typically throw a ThreadAbortException
and will then attempt to terminate the thread.
For example, here we create a new thread and pass it a lambda delegate method that just sleeps for one second before completing. After starting the thread we check the ThreadState
, then call the Abort()
method, which throws a System.Threading.ThreadAbortException
and also causes the ThreadState
to change to Aborted
:
var thread = new Thread(
() => Thread.Sleep(1000)
);thread.Start();
// [00:00:00.0009701] Running
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);
// Exception thrown: 'System.Threading.ThreadAbortException' in mscorlib.dll
thread.Abort();
// [00:00:00.0206406] Aborted
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);
That said, invoking Abort()
on a thread does not guarantee it will be terminated. One such scenario is if the thread contains a finally
block with extensive code. Calling Abort()
triggers such finally
blocks, so an issue in such code could cause a lengthy delay in actual thread termination (or may never abort the thread at all).
One safety precaution is to call the Join
method on the thread after Abort()
is called. This temporarily blocks the calling thread (the thread in which the method was called) until the thread instance associated with the Join()
has finished. Therefore, we can add thread.Join()
to the end of our example above to ensure that the calling thread (Main
thread, in most cases) is blocked until it has finished any final processing:
var thread = new Thread(
() => Thread.Sleep(1000)
);thread.Start();
// [00:00:00.0009701] Running
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);// Exception thrown: 'System.Threading.ThreadAbortException' in mscorlib.dll
thread.Abort();
// [00:00:00.0206406] Aborted
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);
thread.Join();
// [00:00:00.0208147] Aborted
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);
As you may notice by the timestamps, the invocation of Join()
takes place almost instantaneously following the call to Abort()
. This is because, even though our ThreadStart
delegation method attempts to delay processing for one second, Abort()
completes instantly because there's no finally
block holding things up.
To see how integrating a finally
block might work we have the BasicThreadTester
class. Its constructor method creates a new thread and sets its ThreadStart
method to PerformSuspension
. The PerformSuspension()
method outputs a starting message, then contains a finally
block where we've stuck our Thread.Sleep(1000)
delay:
internal class BasicThreadTester
{
internal BasicThreadTester()
{
// Create secondary thread and set name.
var thread = new Thread(PerformSuspension)
{ Name = "Secondary" };// Start thread.
thread.Start();
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);// Sleep one millisecond so process can begin.
Thread.Sleep(1);// Abort thread.
thread.Abort();
Logging.Log(thread.ThreadState, Logging.OutputType.Timestamp);// Join new thread with main thread.
thread.Join();
Logging.Log($"Joining {Thread.CurrentThread.Name} and {thread.Name} threads.", Logging.OutputType.Timestamp);
}
internal void PerformSuspension()
{
try
{
Logging.Log($"{Thread.CurrentThread.Name} thread started.", Logging.OutputType.Timestamp);
}
finally
{
// Delay for one second after abort.
Thread.Sleep(1000);
}
}
}
Instantiating this class results in the following output, which shows that the call to Abort()
was properly delayed that extra one second necessary to execute the finally
block code, before the thread aborted and then joined with the Main
thread:
[00:00:00.0010588] Running
[00:00:00.0013243] Secondary thread started.
The thread 0x15390 has exited with code 0 (0x0).
[00:00:01.0565247] Aborted
[00:00:01.0567066] Joining Main and Secondary threads.
Now that we have a basic understanding of how aborting a thread works, let's see how a System.Threading.ThreadAbortException
might come up. We'll start with the full code sample below, then break it down afterward to see what's going on:
internal class AdvancedThreadTester
{
internal AdvancedThreadTester()
{
// Instantiate thread manager.
var bookManager = new BookManager();// Add Books to Singleton instance List.
bookManager.Singleton.Add(new Book("Magician", "Raymond E. Feist", 681));
bookManager.Singleton.Add(new Book("The Revenant", "Michael Punke", 272));
bookManager.Singleton.Add(new Book("The Final Empire", "Brandon Sanderson", 541));
bookManager.Singleton.Add(new Book("The Code Book", "Simon Singh", 412));
bookManager.Singleton.Add(new Book("Ship of Magic", "Robin Hobb", 880));// Create secondary thread and assign DestroyBooks() as delegate.
var thread = new Thread(
() => bookManager.DestroyBooks())
{ Name = "Secondary" };// Start secondary thread.
thread.Start();
Logging.Log($"{thread.Name} thread started.", Logging.OutputType.Timestamp);// Count Books in main thread
bookManager.CountBooks();// Abort secondary thread.
thread.Abort();
Logging.Log($"{thread.Name} thread aborted.", Logging.OutputType.Timestamp);// Join main and secondary thread.
thread.Join();
Logging.Log($"Joining {Thread.CurrentThread.Name} and {thread.Name} threads.", Logging.OutputType.Timestamp);
}
}internal class BookManager
{
public Singleton<Book> Singleton = Singleton<Book>.Instance;/// <summary>
/// Destroy all Books in Singleton collection and output each.
/// </summary>
/// <param name="delay">Delay between destruction.</param>
internal void DestroyBooks(int delay = 1000)
{
try
{
// Check if any values remain.
while (Singleton.GetValues().IsAny())
{
// Delay processing.
Thread.Sleep(delay);
// Pop (remove) value and output info.
Logging.Log($"Book has been destroyed: {Singleton.Pop().Value}, on {Thread.CurrentThread.Name} thread.", Logging.OutputType.Timestamp);
}
}
catch (System.Threading.ThreadAbortException exception)
{
// Output expected ThreadAbortException.
Logging.Log(exception, true, Logging.OutputType.Timestamp);
}
catch (Exception exception)
{
// Output unexpected Exceptions.
Logging.Log(exception, false, Logging.OutputType.Timestamp);
}
}/// <summary>
/// Count all Books in Singleton collection during loop.
/// </summary>
/// <param name="iterations">Number of iteration loops to perform count.</param>
/// <param name="delay">Delay between counts.</param>
internal void CountBooks(int iterations = 5, int delay = 900)
{
try
{
// Loop once per iteration.
for (var i = 0; i < iterations; i++)
{
// Delay processing.
Thread.Sleep(delay);
// Count books and output.
Logging.Log($"Book count: {Singleton.GetValues().Count}, on {Thread.CurrentThread.Name} thread.", Logging.OutputType.Timestamp);
}
}
catch (System.Threading.ThreadAbortException exception)
{
// Output expected ThreadAbortException.
Logging.Log(exception, true, Logging.OutputType.Timestamp);
}
catch (Exception exception)
{
// Output unexpected Exceptions.
Logging.Log(exception, false, Logging.OutputType.Timestamp);
}
}
}using System;
using System.Diagnostics;namespace Utility
{
/// <summary>
/// Houses all logging methods for various debug outputs.
/// </summary>
public static class Logging
{
/// <summary>
/// Determines type of output to be generated.
/// </summary>
public enum OutputType
{
/// <summary>
/// Default output.
/// </summary>
Default,
/// <summary>
/// Output includes timestamp prefix.
/// </summary>
Timestamp
}/// <summary>
/// Outputs to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="outputType">Output type.</param>
public static void Log(string value, OutputType outputType = OutputType.Default)
{
Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {value}"
: value);
}/// <summary>
/// When <see cref="Exception"/> parameter is passed, modifies the output to indicate
/// if <see cref="Exception"/> was expected, based on passed in `expected` parameter.
/// <para>Outputs the full <see cref="Exception"/> type and message.</para>
/// </summary>
/// <param name="exception">The <see cref="Exception"/> to output.</param>
/// <param name="expected">Boolean indicating if <see cref="Exception"/> was expected.</param>
/// <param name="outputType">Output type.</param>
public static void Log(Exception exception, bool expected = true, OutputType outputType = OutputType.Default)
{
var value = $"[{(expected ? "EXPECTED" : "UNEXPECTED")}] {exception}: {exception.Message}";Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {value}"
: value);
}/// <summary>
/// Outputs to <see cref="Debug.WriteLine(Object)"/>.
///
/// ObjectDumper: http://stackoverflow.com/questions/852181/c-printing-all-properties-of-an-object&lt;/cref
/// </summary>
/// <param name="value">Value to be output to log.</param>
/// <param name="outputType">Output type.</param>
public static void Log(object value, OutputType outputType = OutputType.Default)
{
Debug.WriteLine(outputType == OutputType.Timestamp
? $"[{StopwatchProxy.Instance.Stopwatch.Elapsed}] {ObjectDumper.Dump(value)}"
: ObjectDumper.Dump(value));
}/// <summary>
/// Outputs a dashed line separator to <see cref="Debug.WriteLine(String)"/>.
/// </summary>
public static void LineSeparator(int length = 40)
{
Debug.WriteLine(new string('-', length));
}
}
}using System.Collections.Generic;
namespace Utility
{
/// <summary>
/// Generic Singleton class used to store a List of type T.
/// </summary>
/// <typeparam name="T">Type of objects to store.</typeparam>
public sealed class Singleton<T>
{
/// <summary>
/// Store the singleton instance of this.
/// </summary>
public static Singleton<T> Instance { get; } = new Singleton<T>();/// <summary>
/// List of values.
/// </summary>
private List<T> Values { get; } = new List<T>();static Singleton() { }
private Singleton() { }
/// <summary>
/// Add value to Values List.
/// </summary>
/// <param name="value">Value to add.</param>
public void Add(T value)
{
Values.Add(value);
}/// <summary>
/// Get the current Values List.
/// </summary>
/// <returns>Current Values List.</returns>
public List<T> GetValues()
{
return Values;
}/// <summary>
/// Remove last value and return tuple of index and value.
/// </summary>
/// <returns>Tuple of index and value that was removed.</returns>
public (int Index, object Value) Pop()
{
if (!Values.IsAny()) return (-1, null);
var index = Values.Count - 1;
var value = Values[index];
RemoveAt(index);
return (index, value);
}/// <summary>
/// Remove value from Values List.
/// </summary>
/// <param name="value">Value to remove.</param>
public void Remove(T value)
{
Values.Remove(value);
}
/// <summary>
/// Remove value, via index, from Values List.
/// </summary>
/// <param name="index">Index to remove.</param>
public void RemoveAt(int index)
{
Values.RemoveAt(index);
}
}
}
Our basic goal here is to use a singleton pattern -- which you can learn all about in our Singleton Creation Design Pattern
tutorial -- to share a collection of Books
between two threads. One thread will be actively destroying elements in the collection, while the other thread is outputting the changing Book
count in the collection. While this is a simple example, it illustrates a basic structure that is commonly used to handle shared resources across multi-threaded applications.
We won't go into much detail of the Utility.Singleton<T>
or the Utility.Logging
classes. The former just maintains a singleton instance of itself and implements some methods for manipulating an underlying collection of type T
objects, while the later is used to output to the console.
Instead, we begin with the BookManager
class, which instantiates a Singleton<Book>
instance and implements two basic methods, DestroyBooks(int)
and CountBooks(int, int)
:
internal class BookManager
{
public Singleton<Book> Singleton = Singleton<Book>.Instance;/// <summary>
/// Destroy all Books in Singleton and output each.
/// </summary>
/// <param name="delay">Delay between destruction.</param>
internal void DestroyBooks(int delay = 1000)
{
try
{
// Check if any values remain.
while (Singleton.GetValues().IsAny())
{
// Delay processing.
Thread.Sleep(delay);
// Pop (remove) value and output info.
Logging.Log($"Book has been destroyed: {Singleton.Pop().Value}, on {Thread.CurrentThread.Name} thread.", Logging.OutputType.Timestamp);
}
}
catch (System.Threading.ThreadAbortException exception)
{
// Output expected ThreadAbortException.
Logging.Log(exception, true, Logging.OutputType.Timestamp);
}
catch (Exception exception)
{
// Output unexpected Exceptions.
Logging.Log(exception, false, Logging.OutputType.Timestamp);
}
}
/// <summary>
/// Count all Books in Singleton collection during loop.
/// </summary>
/// <param name="iterations">Number of iteration loops to perform count.</param>
/// <param name="delay">Delay between counts.</param>
internal void CountBooks(int iterations = 5, int delay = 900)
{
try
{
// Loop once per iteration.
for (var i = 0; i < iterations; i++)
{
// Delay processing.
Thread.Sleep(delay);
// Count books and output.
Logging.Log($"Book count: {Singleton.GetValues().Count}, on {Thread.CurrentThread.Name} thread.", Logging.OutputType.Timestamp);
}
}
catch (System.Threading.ThreadAbortException exception)
{
// Output expected ThreadAbortException.
Logging.Log(exception, true, Logging.OutputType.Timestamp);
}
catch (Exception exception)
{
// Output unexpected Exceptions.
Logging.Log(exception, false, Logging.OutputType.Timestamp);
}
}
}
DestroyBooks(int)
loops through the Book
collection of Singleton
while it has values and outputs the result of Singleton.Pop()
, which removes the last element of the collection. CountBooks(int, int)
performs a loop -- once for every iterations
(default 5) and with a delay
of milliseconds (default 500) -- and outputs the current number of values in the Singleton
Book
collection.
As previously mentioned, we'll be using two different threads to test these methods, aborting one midway through to see what happens. The AdvancedThreadTester
class constructor performs all the required logic:
internal class AdvancedThreadTester
{
internal AdvancedThreadTester()
{
// Instantiate thread manager.
var bookManager = new BookManager();// Add Books to Singleton instance List.
bookManager.Singleton.Add(new Book("Magician", "Raymond E. Feist", 681));
bookManager.Singleton.Add(new Book("The Revenant", "Michael Punke", 272));
bookManager.Singleton.Add(new Book("The Final Empire", "Brandon Sanderson", 541));
bookManager.Singleton.Add(new Book("The Code Book", "Simon Singh", 412));
bookManager.Singleton.Add(new Book("Ship of Magic", "Robin Hobb", 880));// Create secondary thread and assign DestroyBooks() as delegate.
var thread = new Thread(
() => bookManager.DestroyBooks())
{ Name = "Secondary" };// Start secondary thread.
thread.Start();
Logging.Log($"{thread.Name} thread started.", Logging.OutputType.Timestamp);// Count Books in main thread
bookManager.CountBooks();// Abort secondary thread.
thread.Abort();
Logging.Log($"{thread.Name} thread aborted.", Logging.OutputType.Timestamp);
// Join main and secondary thread.
thread.Join();
Logging.Log($"Joining {Thread.CurrentThread.Name} and {thread.Name} threads.", Logging.OutputType.Timestamp);
}
}
It starts by instantiating a new BookManager
instance, then adding a small collection of Books
to the underlying Singleton<Book>
instance collection. Next, a new thread called Secondary
is created, to which we assign bookManager.DestroyBooks()
as the delegate method when the thread starts. Speaking of which, we then Start()
the secondary thread and output a message. Then we call bookManager.CountBooks()
, which is invoked on the Main
thread (and was so-named elsewhere in the code).
This is where things become more interesting. Both DestroyBooks()
and CountBooks()
feature loops and intentional delays, so both threads are simultaneously processing for a few seconds, outputting their respective results. Eventually, CountBooks()
completes execution and the Main
thread moves onto the thread.Abort()
call, which forces the Secondary
thread to terminate itself before it finishes destroying all the books. As a safety precaution, we also Join()
both threads together afterward.
The output from this code shows what's going on using relative timestamps, and with indications of which thread is performing what task:
[00:00:00.0031626] Secondary thread started.
[00:00:00.9243493] Book count: 5, on Main thread.
[00:00:01.0262553] Book has been destroyed: 'Ship of Magic' by Robin Hobb at 880 pages, on Secondary thread.
[00:00:01.8247234] Book count: 4, on Main thread.
[00:00:02.0267851] Book has been destroyed: 'The Code Book' by Simon Singh at 412 pages, on Secondary thread.
[00:00:02.7251227] Book count: 3, on Main thread.
[00:00:03.0279273] Book has been destroyed: 'The Final Empire' by Brandon Sanderson at 541 pages, on Secondary thread.
[00:00:03.6258959] Book count: 2, on Main thread.
[00:00:04.0289650] Book has been destroyed: 'The Revenant' by Michael Punke at 272 pages, on Secondary thread.
[00:00:04.5271146] Book count: 1, on Main thread.
[00:00:04.5327573] [EXPECTED] System.Threading.ThreadAbortException: Thread was being aborted.
at System.Threading.Thread.SleepInternal(Int32 millisecondsTimeout)
at System.Threading.Thread.Sleep(Int32 millisecondsTimeout)
at Airbrake.Threading.ThreadAbortException.BookManager.DestroyBooks(Int32 delay) in D:\work\Airbrake.io\Exceptions\.NET\Airbrake.Threading.ThreadAbortException\BookManager.cs:line 23: Thread was being aborted.
[00:00:04.5381438] Secondary thread aborted.
[00:00:04.5382808] Joining Main and Secondary threads.
As we can see, the simultaneous processing worked as intended, allowing the Main
thread to count the dwindling number of Books
while the Secondary
thread set out to destroy them. However, once the Main
thread finished counting and invoked the Abort()
method, our Secondary
thread instantly threw a System.Threading.ThreadAbortException
.
To get the most out of your own applications and to fully manage any and all .NET Exceptions, check out the Airbrake .NET Bug Handler, offering real-time alerts and instantaneous insight into what went wrong with your .NET code, along with built-in support for a variety of popular development integrations including: JIRA, GitHub, Bitbucket, and much more.