当静态构造函数/类型初始化器失败时,来自GC线程的未处理异常

6
使用BLToolkit时,我发现一个有趣的事实——methodInfo.Invoke不总是捕获调用方法中的异常。
例如,在静态构造函数中模拟异常,并通过反射调用该方法。
问题在于TestComponent继承自Component并覆盖了Dispose方法。因此,在这个示例中将会有两个消息——一个“已处理”和一个“未处理”,似乎组件在Reflection的低层级上有不同的处理方式。
如果我们注释掉Dispose(bool disposing)方法,我们将只收到“已处理”消息。
有人能解释一下为什么会发生这种情况并提出解决方案吗?在BLToolkit内部使用try-catch不能作为答案——我不是他们团队的成员 :)
    class Program
{
    static void Main()
    {
        AppDomain.CurrentDomain.UnhandledException +=
            (sender, eventArgs) => Console.WriteLine("unHandled " + eventArgs.ExceptionObject.GetType().FullName);
        try
        {
            try
            {
                var instance = Activator.CreateInstance(typeof(ComponentExecutor));
                MethodInfo mi = typeof(ComponentExecutor).GetMethod("Do");
                BindingFlags bf = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly |
                                  BindingFlags.InvokeMethod;

                mi.Invoke(instance, bf, null, new object[0], CultureInfo.InvariantCulture);
            }
            catch (TargetInvocationException tarEx)
            {
                throw tarEx.InnerException;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Handled " + ex.GetType().FullName);
        }
    }

    class ComponentExecutor
    {
        public void Do()
        {
            new TestComponent().Do();
        }
    }
    class TestComponent : Component
    {
        static TestComponent()
        {
            throw new NullReferenceException();
        }
        [MethodImpl(MethodImplOptions.NoInlining)]
        public void Do()
        {
            Console.WriteLine("Doing");
        }
        protected override void Dispose(bool disposing)
        {
            Console.WriteLine("Disposing");
            base.Dispose(disposing);
        }
    }
}

哦,那很有趣。 - Marc Gravell
我已经在Connect上记录了这个问题:https://connect.microsoft.com/VisualStudio/feedback/details/638078/ - Marc Gravell
1个回答

8

这与反射无关;只需执行以下操作即可获得非常相似的结果:

    AppDomain.CurrentDomain.UnhandledException +=
        (sender, eventArgs) => Console.WriteLine("unHandled " + eventArgs.ExceptionObject.GetType().FullName);
    try
    {
        new TestComponent();
    }
    catch (Exception ex)
    {
        Console.WriteLine("Handled " + ex.GetType().FullName);
    }
    Console.WriteLine("happy");

它实际上写着“happy”;未处理的异常似乎来自GC,可能是因为它试图收集一个“部分构造的对象”(它从未调用实例构造函数),然后这就失败了……特别要注意它们在不同的线程上(我非常确定未处理的异常在GC线程上)。由于是GC/线程化的,很难重现;我在本地添加了GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);,只是为了强制GC发生,以便更频繁地看到它。有趣。
既没有实例构造函数(TestComponent())也没有终结器(~TestComponent())被调用,因此你没有任何可能的方法(据我所知)来解决这个问题。
我唯一能建议的主要事情是:不要让类型初始化器失败:(
我能做的唯一一件事是欺骗运行时:
object obj = FormatterServices.GetUninitializedObject(typeof(TestComponent));

这仍然会触发类型初始化程序并失败,但对象似乎没有那么丢失。也许这条路不会将其标记为终结。因此,GC并不像以前那样讨厌它。

如果类型初始化程序出现故障,您仍将遇到使用该对象的重大问题。

因此,在您的代码中,您可能会有:

class ComponentExecutor
{
    public void Do()
    {
        using (var tc = (TestComponent)
                FormatterServices.GetUninitializedObject(typeof(TestComponent)))
        {
            // call the ctor manually
            typeof(TestComponent).GetConstructor(Type.EmptyTypes).Invoke(tc, null);
            // maybe we can skip this since we are `using`
            GC.ReRegisterForFinalize(tc); 
            tc.Do();
        }
    }
}

现在:

  • 当类型初始化程序失败时,没有任何东西被最终确定(它不需要 - 该对象在这一点上根本无法执行任何操作,因为它从未运行构造函数)
  • 当类型初始化程序正常工作时,构造函数被执行并向GC注册 - 一切顺利

谢谢您的解决方案。但是,与FormatterServices.GetUninitializedObject有什么区别?此对象稍后将被使用(例如Do方法的返回值),因此无法使用。 - Lonli-Lokli
@Lonli - 以前,在部分构造标记为终结的对象后面,类型初始程序(静态.cctor)会失败。这需要收集,但终结器也将失败(同一.cctor)。并且由于终结器在GC线程上运行-非常糟糕。通过使用GetUninitializedObject,我们获得一个未标记为终结的对象,因此即使.cctor失败,我们也没问题。然后我们可以手动修复它。我完全承认这很丑陋和不正规,但它似乎能够工作。 - Marc Gravell
是的,很丑 :) 但是能用。我已经在BLToolkit的页面上添加了问题,但至少现在它应该可以工作了。顺便说一句,我不确定这是否是GC的错误——如果类型初始化程序失败,则调用对象的终结器。 - Lonli-Lokli
1
@Lonli - 我同意,我已经在Connect上记录了:https://connect.microsoft.com/VisualStudio/feedback/details/638078/a-failing-cctor-type-init-on-a-type-with-finalizer-can-cause-gc-thread-to-throw-unhandled-exception - Marc Gravell

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