繁体   English   中英

防止从 BeginInvoke 抛出时丢弃外部异常

[英]Prevent outer exception from being discarded when thrown from BeginInvoke

我有一个Application.ThreadException处理程序,但我发现异常并不总是正确传递给它。 具体来说,如果我从BeginInvoke回调中抛出带有内部异常的异常,我的ThreadException处理程序不会获得外部异常——它只会获得内部异常。

示例代码:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
    var inner = new Exception("Inner");
    var outer = new Exception("Outer", inner);
    //throw outer;
    BeginInvoke(new Action(() => { throw outer; }));
}

如果我取消注释throw outer; 行并单击按钮,然后消息框显示外部异常(连同其内部异常):

System.Exception:外部---> System.Exception:内部
--- 内部异常堆栈跟踪结束 ---
在 C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs:line 55 中的 WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e)
在 System.Windows.Forms.Control.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnClick(EventArgs e)
在 System.Windows.Forms.Button.OnMouseUp(MouseEventArgs 事件)
在 System.Windows.Forms.Control.WmMouseUp(消息和 m,MouseButtons 按钮,Int32 点击)
在 System.Windows.Forms.Control.WndProc(消息和 m)
在 System.Windows.Forms.ButtonBase.WndProc(消息和 m)
在 System.Windows.Forms.Button.WndProc(消息和 m)
在 System.Windows.Forms.Control.ControlNativeWindow.OnMessage(消息& m)
在 System.Windows.Forms.Control.ControlNativeWindow.WndProc(消息和 m)
在 System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd,Int32 msg,IntPtr wparam,IntPtr lparam)

但是如果throw outer; BeginInvoke调用中,如上面的代码,那么ThreadException处理程序获取内部异常。 在调用ThreadException之前,外部异常被剥离,我得到的只是:

System.Exception:内部

(这里没有调用堆栈,因为inner从未被抛出。在一个更现实的示例中,我捕获了一个异常并将其包装以重新抛出,会有一个调用堆栈。)

如果我使用SynchronizationContext.Current.Post而不是BeginInvoke ,也会发生同样的事情:外部异常被剥离,而ThreadException处理程序只获取内部异常。

我尝试在外部包裹更多的异常层,以防它只是剥离最外面的异常,但它没有帮助:显然某处有一个循环在执行while (e.InnerException != null) e = e.InnerException; .

我使用BeginInvoke是因为我的代码需要引发未处理的异常以立即由ThreadException处理,但此代码位于调用堆栈上方的catch块内(具体而言,它位于Task的操作内,并且Task将捕获异常并阻止它传播)。 我正在尝试使用BeginInvoke来延迟throw ,直到下一次在消息循环中处理消息时,当我不再在那个catch中时。 我不喜欢BeginInvoke的特定解决方案; 我只想抛出一个未处理的异常。

即使我在其他人的全部catch中,如何导致异常(包括其内部异常)到达ThreadException

(由于程序集依赖性,我不能直接调用我的ThreadException -handler 方法:处理程序被 EXE 的启动代码挂钩,而我当前的问题是在较低层的 DLL 中。)

一种方法是将内部异常引用放在自定义属性或Data字典中——即,将InnerException属性保留为空,并以其他方式携带该引用。

当然,这需要建立某种可以在抛出代码和处理代码之间共享的约定。 最好的方法可能是在两个代码都引用的项目中定义一个具有自定义属性的自定义异常类。

示例代码(尽管它需要更多注释来解释为什么它正在做它正在做的疯狂事情):

public class ExceptionDecorator : Exception {
    public ExceptionDecorator(Exception exception) : base(exception.Message) {
        Exception = exception;
    }
    public Exception Exception { get; private set; }
}

// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));

// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
    var exception = e.Exception;
    if (exception is ExceptionDecorator)
        exception = ((ExceptionDecorator) exception).Exception;
    // ...
}

这无疑是一种 hack,但它是我能想出的最好的解决方案,它支持 WinForms 中的全局异常处理和所有异常,即使有内部异常也是如此。

特别感谢 yas4891 的回答启发了这个解决方案。

Program.cs中:

internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, true);

        Application.Run(new MyMainForm());
    }


    private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
    {
        _outermostExceptionCache.AddException(e.Exception);
    }


    private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        Exception exception = null;
        if (e?.Exception != null)
            exception = _outermostExceptionCache.GetOutermostException(e.Exception);
        // Handle exception
    }


    private static OutermostExceptionCache _outermostExceptionCache = new();
}

为此,您需要OutermostExceptionCache类:

public class OutermostExceptionCache
{
    public void AddException(Exception ex)
    {
        if ((ex != null) && (ex is not TargetInvocationException))
        {
            Exception innermostException = GetInnermostException(ex);
            lock (_syncRoot)
            {
                RemoveOldEntries();
                _cache[innermostException] = new CacheEntry(ex);
            }
        }
    }


    public Exception GetOutermostException(Exception ex)
    {
        Exception innermostException = GetInnermostException(ex);
        Exception outermostException = null;
        lock (_syncRoot)
        {
            if (_cache.TryGetValue(innermostException, out CacheEntry entry))
            {
                outermostException = entry.Exception;
                _cache.Remove(innermostException);
            }
            else
            {
                outermostException = ex;
            }
        }
        return outermostException;
    }


    private void RemoveOldEntries()
    {
        DateTime now = DateTime.Now;
        foreach (KeyValuePair<Exception, CacheEntry> pair in _cache)
        {
            TimeSpan timeSinceAdded = now - pair.Value.AddedTime;
            if (timeSinceAdded.TotalMinutes > 3)
                _cache.Remove(pair.Key);
        }
    }


    private Exception GetInnermostException(Exception ex)
    {
        return ex.GetBaseException() ?? ex;
    }


    private readonly object _syncRoot = new();
    private readonly Dictionary<Exception, CacheEntry> _cache = new();


    private class CacheEntry
    {
        public CacheEntry(Exception ex)
        {
            Exception = ex;
            AddedTime = DateTime.Now;
        }


        public Exception Exception { get; }
        public DateTime AddedTime { get; }
    }
}

它的工作方式是在运行时甚至将异常冒泡到最近的 catch 块之前观察每个异常,因为它被抛出。 每次抛出异常时,都会将其添加到缓存中,并由最内层(即基本)异常进行索引。 因此,当一个异常被捕获并抛出一个新异常时,以原来的异常作为其内部异常,缓存将使用该外部异常进行更新。 然后,当Application.ThreadException事件处理程序提供了未包装的最内层异常时,处理程序可以从缓存中查找最外层的异常。

注意:由于即使是本地捕获的异常也会被添加到缓存中(因此永远不会通过调用GetOutermostException来删除),它会为每个异常添加时间戳并自动丢弃任何超过 3 分钟的异常。 这是一个任意超时,可以根据需要进行调整。 如果将超时设置得太短,可能会导致调试出现问题,因为如果在调试器中暂停进程太久(在抛出异常之后但在处理之前),它可能会导致异常处理恢复为仅处理最里面的异常)。

我假设您在 x64 Windows 系统上看到了这种行为,这是一个 - 相当未知 - x64 Windows 的实现细节。 这里阅读

这篇文章详细介绍了如何通过应用一些修补程序来解决这个问题,据称这些修补程序是随 Win7 SP1 一起提供的,但几周前我在 Win7 SP1 上遇到了这个问题。

此外,您可以附加到AppDomain.FirstChanceException 事件,该事件使您可以在将每个异常传递给 CLR 进行处理之前访问它

将异常传播到更高层的推荐方法(除了通过等待任务隐式重新抛出)是删除任务主体中的全部内容,而是使用Task.ContinueWith在任务上注册故障延续,指定TaskContinuationOptions.OnlyOnFaulted 如果您正在通过中间层工作并且无权访问任务,则可以进一步将其包装在您自己的 UnhandledException 事件中以向上传递 Exception 对象。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM