Q. How does ThreadAbortException really work? I noticed that it's implemented as an internal call to the CLR EE, and the MSDN docs don't really say much. The interesting thing about this particular exception is that it acts asynchronously (or seems to), and I'm just wondering when and how the CLR can "hijack" the thread to be aborted.

Asked by Kevin Lanos. Answered by the Wonk on January 20, 2003

A.

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, when aborting a thread, that 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

}

 

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 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 that I know of. 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 starts running again, the scheduler will call the APC, which sets the thread to abort state to AbortInitiated
  6. When an thread is interruptible, it will return to execution via a “trip” function, which checks all kinds of state for special activity
  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 looking 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 (Common Language Runtime) 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; clears the mind and purifies the soul.

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

Feedback

I have feedback on this Ask The Wonk answer