Threads can't be aborted while they're running code inside a catch/finally block

(and more on the exciting world of thread aborts)

It logically makes sense, if threads could be aborted while they were inside catch or finally blocks, those constructs would be fairly useless.  For example:

            SafeHandle sh = null;
            try
            {
                sh = AllocateUnmanagedResource();
            }
            finally
            {
                FreeUnmanagedResource(sh);
            }

If threads were abortable in finally blocks, there would be no guarantee that the resource allocated in the try block would ever be freed (note, this pattern is basically a using block). This rule doesn't apply to "rude" thread aborts, which are used in some hosting scenarios (SQLCLR for instance) and basically boil down to a call to TerminateThread(), but code enabling rude aborts generally is written to not even allow code that could leak to run.  For instance, SQLCLR uses the CLR verifier and HPA to ensure code running in its domains won't leak if threads are terminated.

I came across this trying to investigate why some requests in an ASP.NET app would never time out. The culprit was code someone wrote similar to this:

            try
            {
                RunSlowOperation();
            }
            catch (Exception)
            {
                try
                {
                    RunOtherSlowOperation();
                }
                catch (Exception)
                {
                    // Give up
                }
            }

The idea was to try to do a (possibly long running) operation in one way, and if it failed, try it a different way. Besides being a horrible abuse of exception handling, the thread stops being abortable while RunOtherSlowOperation is executing, and thus can't be timed out by ASP.NET.  The solution is simple, refactor the code to be something like this:

            bool doOtherStuff = false;
            try
            {
                RunSlowOperation();
            }
            catch (Exception)
            {
                doOtherStuff = true;
            }
            if (doOtherStuff)
            {
                try
                {
                    RunOtherSlowOperation();
                }
                catch (Exception)
                {
                    // Give up
                }
            }

This way the time in catch blocks is reduced to a trivial amount of time.

An interesting side note is that I've never been able to find any accurate information online about how thread aborts work and abortable conditions.  Most documentation only talks about aborting code inside an alertable wait/sleep/join (most locking primitives enter an alertable state) using APCs and QueueUserAPC.  This isn't the whole story however.  Consider this trivial case:

while (true)
{
    x++;
}

This thread never enters an alertable wait state where it could process an APC, but it's definitely abortable.  As far as I can tell, this feature is NOT implemented in the SSCLI 2.0.  The code is in Thread::UserAbort, src\clr\vm\threads.cpp, around line 5048 and doesn't seem to handle this case.

I haven't spent a lot of time looking at the actual implementation but I suspect it works similar to this:

(Updated: 6/14/11, I looked into it and this is basically how the CLR does it.)

  1. Ensure the target thread is at a safe point (running managed code)
  2. Suspend the thread
  3. Use GetThreadContext to extract the IP and any registers that will be touched by the next steps from the thread's context.
  4. Generate a small stub to:
    1. Set up the stack to correctly unwind back to the IP saved in (3) (as if the stub had been called from the target thread).
    2. Throw a thread abort exception.
  5. Use SetThreadContext to change the IP (and maybe some other registers if needed) to point to the stub's entry point.
  6. Resume the thread

When the target thread resumes after step 7, it'll begin running inside out stub, which will handle setting up the stack and throwing the ThreadAbortException.

The (possibly) full list of abortable states that will abort (almost) immediately is as follows:

  1. Performing an alertable wait/sleep/join [will abort when the APC gets processed, typically immediately]
  2. In managed code at a safe point  (not in a catch/finally for example) [will abort immediately]
  3. Suspended for GC [will abort when the GC finishes]

Then there's a couple strange cases that don't abort immediately:

  1. User suspended (with Thread.Suspend()) threads will be aborted when they are resumed, but the call to Abort() will also throw an exception.
  2. Threads in native code will be aborted when they transition back into the CLR.

Hopefully this clears up some confusion.


© 2023. All rights reserved.