Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does an unhandled exception in this background thread not terminate my process?

I spawn a foreground thread and a background thread, throwing an exception in each.

using System;
using System.Threading;

namespace OriginalCallStackIsLostOnRethrow
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                A2();

                // Uncomment this to see how the unhandled
                // exception in the foreground thread causes
                // the program to terminate
                // An exception in this foreground thread
                // *does* terminate the program
                // var t = new Thread(() => {
                //     throw new DivideByZeroException();
                // });

                // t.Start();
            }
            catch (Exception ex)
            {
                // I am not expecting anything from the
                // threads to come here, which is fine
                Console.WriteLine(ex);
            }
            finally
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }

        static void A2() { B2(); }
        static void B2() { C2(); }
        static void C2() { D2(); }
        static void D2()
        {
            Action action = () => 
            {
                Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
                throw new DivideByZeroException();
                Console.WriteLine("Do we get here? Obviously not!");
            };
            action.BeginInvoke(ar => Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}"), null);
        }
    }
}

As expected, the unhandled exception in the foreground thread terminates the process. However, the unhandled exception in the background thread just terminates the thread and does not bring the process to a halt, effectively going unobserved and failing silently.

This program, therefore, produces the following output:

Press any key to exit...
D2 called on worker #6. Exception will occur while running D2
D2 completed on worker thread #6

This challenges my understanding about exception handling in threads. My understanding was that regardless of the nature of the thread, an unhandled exception, from v2.0 of the framework onwards, will bring the process to termination.

Here is a quote from the documentation on this topic:

The foreground or background status of a thread does not affect the outcome of an unhandled exception in the thread. In the .NET Framework version 2.0, an unhandled exception in either foreground or background threads results in termination of the application. See Exceptions in Managed Threads.

Further more, the page titled Exceptions in Managed Threads states as follows:

Starting with the .NET Framework version 2.0, the common language runtime allows most unhandled exceptions in threads to proceed naturally. In most cases this means that the unhandled exception causes the application to terminate.

This is a significant change from the .NET Framework versions 1.0 and 1.1, which provide a backstop for many unhandled exceptions — for example, unhandled exceptions in thread pool threads. See Change from Previous Versions later in this topic.

ANOTHER INTERESTING OBSERVATION

Interestingly, if I cause the exception to be thrown in the completion callback instead of the actual action that is being done, the exception on the background thread in that case does cause a termination of the program. For code, please see below.

using System;
using System.Threading;

namespace OriginalCallStackIsLostOnRethrow
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // A2();
                A3();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }

        static void A2() { B2(); }
        static void B2() { C2(); }
        static void C2() { D2(); }
        static void D2()
        {
            Action action = () => 
            {
                try
                {
                    Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
                    throw new DivideByZeroException();
                    // Console.WriteLine("Do we get here? Obviously not!");
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex);
                }
            };
            action.BeginInvoke(ar => Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}"), null);
        }

        static void A3() { B3(); }
        static void B3() { C3(); }
        static void C3() { D3(); }
        static void D3()
        {
            Action action = () => { Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}."); };
            action.BeginInvoke(ar =>
            {
                Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}. Oh, but wait! Exception!");

                // This one on the completion callback does terminate the program
                throw new DivideByZeroException();
            }, null);
        }
    }
}

YET ANOTHER INTERESTING OBSERVATION

Further, even more interestingly, if you handle the exception in the action that you want to execute using APM, in the catch block (set a breakpoint in the catch block in D2()), the Exception that appears has no stack trace other than the lambda being invoked. It has absolutely no information even about how it got there.

Whereas this is not true for exceptions that you trap in a catch block in the completion callback, as in the case of D3().

I am using the C# 6.0 compiler in Visual Studio Community 2015 Edition and my program targets v4.5.2 of the .NET framework.

like image 956
Water Cooler v2 Avatar asked Jun 05 '16 19:06

Water Cooler v2


1 Answers

As PetSerAl points out in the comments section of the question, to get the exception information, it is mandatory to call EndInvoke from inside the completion callback as shown below.

using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;

namespace OriginalCallStackIsLostOnRethrow
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                A2();
                // A3();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
        }

        static void A2() { B2(); }
        static void B2() { C2(); }
        static void C2() { D2(); }
        static void D2()
        {
            Action action = () => 
            {
                Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
                throw new DivideByZeroException();    
            };
            action.BeginInvoke(ar =>
            {
                ((Action)((ar as AsyncResult).AsyncDelegate)).EndInvoke(ar);

                Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}");
            }, null);
        }

        static void A3() { B3(); }
        static void B3() { C3(); }
        static void C3() { D3(); }
        static void D3()
        {
            Action action = () => { Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}."); };
            action.BeginInvoke(ar =>
            {
                try
                {
                    Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}. Oh, but wait! Exception!");
                    throw new DivideByZeroException();
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }, null);

        }
    }
}

This is weird, and it still remains a mystery as to why the stack trace does not show up if you were to place a try / catch block in the action that is executing asynchronously.

I am referring to the absence of the StackTrace, not the absence of a call stack. :-)

enter image description here

like image 192
Water Cooler v2 Avatar answered Oct 06 '22 01:10

Water Cooler v2