3

While investigating an issue with finally, await, and ThreadAbortException, I came another quirk. According to the documentation:

ThreadAbortException is a special exception that can be caught, but it will automatically be raised again at the end of the catch block.

But consider this console program:

class Program
{
    static void Main()
    {
        Run(false).GetAwaiter().GetResult();
        Run(true).GetAwaiter().GetResult();
    }

    static async Task Run(bool yield)
    {
        Console.WriteLine(yield ? "With yielding" : "Without yielding");
        try
        {
            try { await Abort(yield); }
            catch (ThreadAbortException)
            {
                Console.WriteLine("    ThreadAbortException caught");
            } // <-- ThreadAbortException should be automatically rethrown here
        }
        catch (ThreadAbortException)
        {
            Console.WriteLine("    Rethrown ThreadAbortException caught");
            Thread.ResetAbort();
        }
    }

    static async Task Abort(bool yield)
    {
        if (yield)
            await Task.Yield();
        Thread.CurrentThread.Abort();
    }
}

When I compile this in Visual Studio 2015, the output is:

Without yielding
    ThreadAbortException caught
    Rethrown ThreadAbortException caught
With yielding
    ThreadAbortException caught

So a ThreadAbortException raised after Task.Yield() is no longer automatically rethrown by a catch block! Why is this?

Michael Liu
  • 52,147
  • 13
  • 117
  • 150

1 Answers1

2

The reason why it happens if you don't await Task.Yield is that the code is executed synchronously on the same thread as the caller so it's like not being async at all.

When you await, the continuation will be queued on a ThreadPool thread which is a managed thread and behaves differently.

Since internally is caught and re-thrown from a different thread than the current, it doesn't preserve it's nature of "special application killing" exception in the shifting logic.

In addition, if it was to re-throw it, you wouldn't even been able to Thread.ResetAbort() as it works on the current thread and will not act on the one that actually aborted.

MSDN documentation explains this, too here:

If any of these exceptions are unhandled in threads created by the common language runtime, the exception terminates the thread, but the common language runtime does not allow the exception to proceed further.

If these exceptions are unhandled in the main thread, or in threads that entered the runtime from unmanaged code, they proceed normally, resulting in termination of the application.

My guess on the rationale behind this is: ThreadAbortException is re-thrown to avoid stubborn threads to try and stay alive when they shouldn't, but letting it flow and kill also other different threads would probably be quite a bad idea and cause unexpected behaviour.

Stefano d'Antonio
  • 5,874
  • 3
  • 32
  • 45
  • Would these not be considered handled exceptions? Why not? – Jonathon Chase Jan 12 '18 at 16:39
  • @JonathonChase If I understood your question (why catching re-throws if we're catching it!?) I can only imagine that is a legacy behaviour that used to kill the application if the main thread was aborted. The article above calls them `The common language runtime provides a backstop for certain unhandled exceptions that are used for controlling program flow` which is not entirely clear. – Stefano d'Antonio Jan 12 '18 at 16:41
  • Sorry, I wasn't clear. The documentation you referenced discussed the behavior of unhandled exceptions. My understanding is that these exceptions are handled by the catch statements. – Jonathon Chase Jan 12 '18 at 16:43
  • You're right, it calls them `unhandled` exceptions, but it doesn't explain why it stays unhandled after catching it. This part of the documentation explains the special case in the *Remarks* section: https://msdn.microsoft.com/en-us/library/5b50fdsz(v=vs.110).aspx – Stefano d'Antonio Jan 12 '18 at 16:48
  • *"When this method is invoked on a thread, the system throws a ThreadAbortException in the thread to abort it. ThreadAbortException is a special exception that can be caught by application code, but is re-thrown at the end of the catch block unless ResetAbort is called."* from the docs. – Stefano d'Antonio Jan 12 '18 at 16:49
  • Which I think brings us back around to the crux of the question: why isn't the exception rethrown, or if it is, why is it not caught, if the catch contains an await statement? – Jonathon Chase Jan 12 '18 at 16:53
  • 2
    As I mentioned in the answer, I think it's because it's on a different thread. It is re-thrown to avoid stubborn threads to try and stay alive when they shouldn't and unwind the call stack entirely, but letting it kill other threads would probably be quite bad. – Stefano d'Antonio Jan 12 '18 at 16:55
  • @JonathonChase I think it is correct explanation. You can throw ThreadAbortException all day and it *will not* be automatically re-thrown as it is *not-so-special* when created any other way than `.Abort` (same as `throw new StackOverflowExcepction()` - which kills the process if raised due to actual SO). (Throwing ThreadAbort directly a bit tricky as there is no public constructor, but you can always - `throw (Exception)(typeof(ThreadAbortException).GetConstructor(BindingFlags.NonPublic|BindingFlags.Instance, null, CallingConventions.Any, new Type[0], null).Invoke(new object[0] {})); `) – Alexei Levenkov Jan 12 '18 at 17:17
  • @AlexeiLevenkov That makes sense, and would help explain why issuing a `throw;` in the inner catch would cause the `Thread.ResetAbort();` in the outer catch to fail with `Unable to reset abort because no abort was requested.` I'm guessing that by this point the thread has been dealt with by the CLR, which is why the re-throw isn't occurring on it's own. – Jonathon Chase Jan 12 '18 at 17:24
  • I'd like to understand the details of how this happens, with citations to Reference Source if possible. For example, is this behavior defined by the thread pool, by the Task class, or by something else? Would this same behavior happen with SynchronizationContexts that don't use the thread pool? – Michael Liu Jan 12 '18 at 17:37
  • @MichaelLiu with a SyncContext it will depend on the implementation. ASP.NET/WinForms/WPF dispatch the behaviour on the main UI thread, so I presume it could re-throw in that case (to test), but if you use `ConfigureAwait(false)` for sure it will behave the same. By source you mean source code? Reference to the documentation is in the links I shared. – Stefano d'Antonio Jan 12 '18 at 20:33
  • Don't take my word for it on the re-throw thing as there will still be switching... So the async code may still run on a `ThreadPool` thread and I don't know if switching back will keep the old behaviour. – Stefano d'Antonio Jan 12 '18 at 20:35
  • By "Reference Source" I mean the [.NET Framework source code](http://referencesource.microsoft.com/). – Michael Liu Jan 12 '18 at 20:55