簡體   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