AddThis Social Bookmark Button

Print

Plumbing the Depths of the ThreadAbortException Using Rotor

by Chris Sells, coauthor of Mastering Visual Studio .NET
02/18/2003

I was asked the other day how calling Thread.Abort could raise an exception in a completely different thread. Finding the answer led me a merry chase.

Threads & Exceptions

An exception is something that causes an alternate path of execution on the current thread. As an example, consider the following:


static void Foo() {
  throw new Exception("oops!");
  Console.WriteLine("Never going to get here...");
}

static void Main(string[] args) {
  try {
    Foo();
    Console.WriteLine("Never going to get here, either...");
  }
  catch( Exception ex ) {
    Console.WriteLine("Exceptions happen: " + ex.Message);
  }
}

Throwing an exception from the Foo method will abort the rest of the statements in the Foo and resume execution in the catch block in the Main method. However, while the flow of execute has been changed, the thread of execution continues, that is, the throw and the catch both happen on the same thread. However, aborting a thread causes an exception to be thrown on another thread:


static void Foo() {
  try { while( true ) { ... } }
  catch( ThreadAbortException ex ) { ... }
  finally {...}

  // Will never get here if thread aborted
}

static void Main(string[] args) {
  Thread thread = new Thread(new ThreadStart(Foo));
  thread.Start();
  thread.Abort(); // cause ThreadAbortException to be thrown
}

Related Reading

Shared Source CLI Essentials
By David Stutz, Ted Neward, Geoff Shilling

In this example, method Foo is running on separate thread and when it's aborted (via the Thread.Abort method), the flow of execution is aborted, causing the catch block to be executed. This is a handy way to communicate with a worker thread that it's no longer needed, so it should clean up and get out. In fact, the thread abort exception is such a strong statement that after the catch and/or finally blocks are executed, no more lines of code on that thread are allowed to execute. Conversely, if the thread has already been aborted, the catch block and the finally block can continue to execute indefinitely, giving the thread the final say as to whether it allows itself to be aborted or not.

Implementation Details

However, the fact that a thread being aborted is allowed to execute any lines of code is quite an accomplishment. No Win32 APIs provide this functionality. If a Win32 thread is aborted and/or terminated from the outside, that's it--a thread has no defense, not even to clean up. .NET is providing a nice little bit of functionality by giving us the ability to catch an abort and deal with it, especially since this functionality is not provided directly by the underlying OS.

The way .NET implements Thread.Abort from one thread on another is a multi-step process:

  1. Suspend the underlying OS thread to abort.
  2. Set the .NET thread to abort state to include the AbortRequested bit.
  3. Wait for the thread to abort to be interruptible, that is, sleeping, waiting, or joining.
  4. Add an asynchronous procedure call (APC) to the thread's APC queue (using the Win32 function QueueUserAPC) and resume the thread.
  5. When the thread to abort moves into an alertable wait state, the scheduler will call the APC, which sets the thread to abort state to AbortInitiated. A thread enters an alertable wait state only when it passes TRUE as bAlertable to a function like SleepEx. If that doesn't happen, the APC queue is never serviced, which means that Thread.Abort is not guaranteed to cause an exception in the thread to abort.
  6. When the Common Language Runtime (CLR) gets back control of the thread to abort, it will return to execution via a "trip" function, which checks all kinds of states for special activity, including if the thread should be aborted.
  7. If the thread's state is set to AbortInitiated, throw the ThreadAbortException, which the thread being aborted can handle via catch and/or finally (or not, as it chooses).

The call to Thread.Abort boils down to .NET setting a flag on a thread to be aborted and then checking that flag during certain points in the thread's lifetime, throwing the exception if the flag is set.

How I Figured this Out

I figured out how Thread.Abort seems to be able to throw an exception from one thread to another by downloading and digging through the Rotor source code (see the References section). Since threads are a low-level primitive (unlike WinForms or ASP.NET), the Rotor source code provides the implementation and lets us figure out exactly how things really work (like the tidbit about what happens when an aborted thread in the catch or finally block is aborted again).

In this case, I searched for the managed Thread implementation (clr\src\bcl\system\threading\thread.cs) and looked up the implementation of Thread.Abort, which lead me to AbortInternal:


namespace System.Threading {
  public sealed class Thread {
  ...
    public void Abort() { AbortInternal(); }

    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    private extern void AbortInternal();
  }
}

Since AbortInternal is marked as an internal call, I knew I'd be looking for a C++ class that implemented it, which I found by searching for AbortInternal in .h and .cpp files. What I found was not what I expected, although in retrospect, I'm unsurprised. In clr\src\vm\ecall.cpp, I found a map entry for the AbortInternal internal call:


static
ECFunc gThreadFuncs[] = {
  ...
  {FCFuncElement("AbortInternal", NULL, (LPVOID)ThreadNative::Abort)},    
  ...
};

This mapping provides the CLR with the address to the function to call as the implementation of AbortInternal. The C++ ThreadNative class provides the declaration and definition of the method in clr\src\vm\threads.h and clr\src\vm\threads.cpp, respectively. After that, it was a pleasant 30 minutes or so treading through the Abort (and UserAbort) methods, figuring out who triggered what. I highly recommend this kind of activity to you and all your friends; it clears the mind and purifies the soul.

I also asked the King of Threads, Mike Woodring, to look over my assessment of the situation. He's the one that clued me into the need for a thread to be in an alertable wait state before APC requests would be handled. Thanks, Mike!

Where Are We?

Throwing an exception from one thread to another works nicely because the CLR handles these activities for us instead of requiring us to implement them ourselves (as Win32 would require). This is yet another example of the virtualization of the platform stepping in to provide services that aren't provided by the underlying OS.

And how did I figure it out? All things are revealed in the true documentation for any hunk of software, by which I mean, of course, the source code.

References

Chris Sells is a Microsoft Software Legend and a Program Manager with the Distributed Systems Group at Microsoft. His weblog at http://www.sellsbrothers.com is popular with .NET developers for its zany and independent commentary on technology and geek culture.


Return to ONDotnet.com