[轉]WPF 使用 Dispatcher 的 InvokeAsync 和 BeginInvoke 的異常處理差別

一般認爲 WPF 的 Dispatcher 的 InvokeAsync 方法是 BeginInvoke 方法的平替方法和升級版,接近在任何情況下都應該在業務層使用 InvokeAsync 方法代替 BeginInvoke 方法。然而在異常的處理上,這兩個方法還是有細微的差別的,不能說是壞事,依然可以認爲使用 InvokeAsync 方法代替 BeginInvoke 方法是正確的。本文將記錄這兩個在拋出異常時,進入的統一異常處理事件的差別

簡單來說是在 InvokeAsync 拋出未捕獲的異常,將會進入到 TaskScheduler.UnobservedTaskException 事件裏面。在 BeginInvoke 拋出未捕獲的異常,將會進入到 Dispatcher.UnhandledException 事件裏面

根據通用的 dotnet 知識可以知道,進入到 TaskScheduler.UnobservedTaskException 的異常,在 .NET Framework 4.5 之後,包含 dotnet core 和 dotnet 5 和 dotnet 6 以及更高版本,是不會導致應用程序退出進程

根據通用的 WPF 知識可以知道,進入到 Dispatcher.UnhandledException 的異常,取決於參數的 Handled 屬性是否被設置爲 true 值,決定是否將異常拋到線程頂層從而可能導致應用程序退出進程

通過此可以瞭解到,使用 InvokeAsync 和 BeginInvoke 所拋出的未捕獲異常所進入的事件不相同。這裏值得說明的是,無論是 InvokeAsync 或 BeginInvoke 方法,都沒有使用其返回值。進一步的說明就是不對 InvokeAsync 使用 await 等待的前提下,表現行爲如本文描述。本文開始的說法是嚴謹的,因爲對 InvokeAsync 使用 await 等待,則將 InvokeAsync 異常交給 await 這一端,然後取決於等待的邏輯的異常處理,此時和 InvokeAsync 行爲無關

有一些不符合我開始預期的是 InvokeAsync 拋出未捕獲的異常,將會進入到 TaskScheduler.UnobservedTaskException 事件裏面。這是因爲 InvokeAsync 走的是 Task 封裝。在 dotnet 裏面,如果 Task 裏存在異常,且此 Task 沒有任何的 await 將會在此 Task 被回收清理時,將異常記錄到 TaskScheduler.UnobservedTaskException 事件

接下來是對此行爲的測試代碼

新建一個 WPF 項目,編寫簡單的界面,加上兩個按鈕,這兩個按鈕用來分別調用 InvokeAsync 和 BeginInvoke 拋出異常

<Window x:Class="GifellichelNurcikaifallhane.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GifellichelNurcikaifallhane"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <TextBlock x:Name="TextBlock" Margin="10,10,10,10" 
                   TextWrapping="Wrap"/>
        <StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Top">
            <Button x:Name="InvokeAsyncButton" Margin="10,10,10,10"
                    Click="InvokeAsyncButton_OnClick">InvokeAsync</Button>
            <Button x:Name="BeginInvokeButton" Margin="10,10,10,10"
                    Click="BeginInvokeButton_OnClick">BeginInvoke</Button>
        </StackPanel>
    </Grid>
</Window>

 

在 MainWindow 的構造函數裏面,分別添加兩個異常捕獲事件,用來進行輸出。同時跑一個任務不斷執行垃圾回收

 public MainWindow()
    {
        InitializeComponent();

        Dispatcher.UnhandledException += (sender, args) =>
        {
            args.Handled = true;
            TextBlock.Text += $"Dispatcher UnhandledException {args.Exception.Message}\r\n";
        };

        TaskScheduler.UnobservedTaskException += (sender, args) =>
        {
            Dispatcher.InvokeAsync(() =>
            {
                TextBlock.Text += $"TaskScheduler UnobservedTaskException {args.Exception.InnerException!.Message}\r\n";
            });
        };

        Task.Run(async () =>
        {
            while (true)
            {
                // 不斷 GC 方便 Task 清理
                await Task.Delay(TimeSpan.FromSeconds(1));
                GC.Collect();
            }
        });
    }

 

以上代碼裏面,因爲 TaskScheduler 的 UnobservedTaskException 不是在主線程調度的,需要使用 Dispatcher 才能讓內容輸出在界面

接下來編寫兩個按鈕的代碼

    private void InvokeAsyncButton_OnClick(object sender, RoutedEventArgs e)
    {
        Dispatcher.InvokeAsync(() => throw new Exception($"在 Dispatcher.InvokeAsync 拋出異常"));
    }

    private void BeginInvokeButton_OnClick(object sender, RoutedEventArgs e)
    {
        Dispatcher.BeginInvoke(new Action(() => throw new Exception($"在 Dispatcher.BeginInvoke 拋出異常")));
    }

這裏需要特別說明的是,咱是不應該拋出 Exception 類型的異常的,正確的做法是拋出特別類型的異常,例如 ArgumentException 等類型的異常。以上的代碼僅用來進行測試行爲

運行以上代碼,分別點擊兩個按鈕,可以看到有不同的輸出,從而可以瞭解到這兩個方法的異常處理行爲

本文的代碼放在github 和 gitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin a7cbc4bd5e0ec41be5d0be719fa387adfb6bf52e

 

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin a7cbc4bd5e0ec41be5d0be719fa387adfb6bf52e

 

獲取代碼之後,進入 GifellichelNurcikaifallhane 文件夾

轉自 https://www.cnblogs.com/lindexi/p/17679628.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章