为什么在后台线程中出现未处理的异常不会终止我的进程?

6

我会生成一个前台线程和一个后台线程,在每个线程中抛出异常。

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);
        }
    }
}

作为预期,前台线程中未处理的异常会终止进程。然而,在后台线程中未处理的异常仅终止线程,不会使进程停止,实际上是不被观察到且默默失败的。
因此,该程序产生以下输出:
Press any key to exit...
D2 called on worker #6. Exception will occur while running D2
D2 completed on worker thread #6

这挑战了我对线程中异常处理的理解。我的理解是,无论线程的性质如何,从框架v2.0开始,未处理的异常都会导致进程终止。
以下是关于此主题的文档引用:

线程的前台或后台状态不会影响线程中未处理异常的结果。在.NET Framework 2.0中,无论是前台线程还是后台线程中的未处理异常都会导致应用程序终止。请参阅托管线程中的异常。

此外,名为托管线程中的异常的页面如下所述:
自.NET Framework 2.0版本开始,公共语言运行时允许大多数线程中未处理的异常自然进展。在大多数情况下,这意味着未处理的异常会导致应用程序终止。这与.NET Framework 1.0和1.1版本有很大的不同,后者为许多未处理的异常提供了后备支持,例如线程池线程中的未处理异常。请参见本主题后面的“与以前版本的差异”。
另一个有趣的观察是,如果我在完成回调中引发异常而不是正在执行的实际操作,则该情况下后台线程中的异常确实会导致程序终止。有关代码,请参见下文。
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);
        }
    }
}

另一个有趣的观察

更加有趣的是,如果你在使用APM执行操作时处理异常,在catch块中(在D2()的catch块中设置断点),出现的Exception除了调用lambda表达式之外没有堆栈跟踪信息。它甚至没有关于如何到达那里的任何信息。

然而,在捕获完成回调中的catch块中的异常时,情况并非如此,就像D3()的情况一样。

我正在使用Visual Studio Community 2015 Edition的C# 6.0编译器,我的程序目标是.NET框架v4.5.2。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Water Cooler v2
你说得对,这个警告对于问题的背景并没有太大关系。 - Chris O
1
我也感到惊讶。我看到了这个链接,可能会有所启示:https://dev59.com/DGw15IYBdhLWcg3wqNqM。我见过类似的情况只有一次,那是在一个 C# Windows 服务中的线程调用 GetDirectories 时,我们糟糕的网络失去了与线程正在查看的共享驱动器的连接。线程没有失败,而是停滞了下来,服务继续运行而没有任何警告。这相当令人恼火,因为该线程应该执行重要任务。 - John D
2
你应该调用 EndInvoke 来传播异步方法调用的结果/异常。 - user4003407
你经历了我试图在D2()中强调的危险。 - Water Cooler v2
显示剩余3条评论
1个回答

3

正如PetSerAl在评论中指出的那样,在完成回调函数内部获取异常信息,必须像下面展示的那样调用EndInvoke

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);

        }
    }
}

这很奇怪,如果你在异步执行的操作中放置一个try / catch块,为什么堆栈跟踪不会显示仍然是个谜。

我指的是StackTrace的缺失,而不是调用堆栈的缺失。 :-)

enter image description here


调用堆栈是存在的。只是您的委托代码是您编写代码的第一帧。让Visual Studio显示外部代码。 - usr
@usr 当然,调用堆栈必须存在。我指的是缺少堆栈跟踪信息。请查看更新答案中的图片。 :-) - Water Cooler v2
@usr 我看到我命名空间的方式可能会让你误以为我在这个问题中最后一个较小的问题是关于调用堆栈的。 - Water Cooler v2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接