【例外】await で待つ処理内で起きた未処理例外はUIスレッドで捕捉される

C#
スポンサーリンク

例外対応の記事を書こうとしてたら意図通りにならない

こんにちは、働くC#プログラマーのさんさめです。

未処理例外の対応に関する記事を書こうとして検証していたら、はまりました。

さんさめ
さんさめ

UnovserbedTaskExcetionイベントが発火しない…?
というかDispatcherUnhandledExceptionで捕捉できるぞ?

未処理例外の捕捉(と握りつぶし)に使えるイベントは、
UIスレッドで起きた例外を処理するDispatcherUnhandledExceptionと、
それ以外のスレッドで起きた例外を処理する UnovserbedTaskException
の2つがあります。

しかし、以下のコードを実行して「例外発生させる」ボタンを押すと、
UIスレッドで起きた例外としてすぐに異常終了してしまいます。

<Window x:Class="ErrorReport.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:ErrorReport"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Button Click="Button_Click" Content="例外発生させる"/>
        <Button Click="Button_Click_1" Content="強制GC"/>
    </StackPanel>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
    }

    private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        e.SetObserved();
        e.Exception.Handle(ex =>
        {
            MessageBox.Show("申し訳ありません。\n" +
            "お使いのアプリケーションは異常を検知したため終了します\n" +
            "-- エラー内容 --\n" +
            ex.ToString(),
            "異常終了");
            Environment.Exit(1);
            return true;
        });

    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        await Task.Run(() =>
        {
            string a = null;
            MessageBox.Show(a.ToString()); // ここですぐアプリが落ちてしまう
        });
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

書いていたときはどうしてか分からなかったのですが、
一晩置いてみたら答えが分かりました。

さんさめ
さんさめ

awaitで待ってるからか…

awaitでTask.Runの処理を待っているためUIスレッド上の例外として捕捉されたようです。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    // ↓でawaitで待ってる
    await Task.Run(() => 
    {
        string a = null;
        MessageBox.Show(a.ToString());
    });
}

awaitで待たない場合はちゃんと捕捉される

そこで、以下のように書き換えてみたところ、きちんと
UIスレッド以外の方のイベント(UnobservedTaskException)が発火しました。

(※発火されることを確認するためには、
例外を起こした後「強制GC」ボタンを押す必要があります)

private void Button_Click(object sender, RoutedEventArgs e)
{
    // 待たずに投げっぱなしにしてみた
    Task.Run(() => 
    {
        string a = null;
        MessageBox.Show(a.ToString());
    });
}

ConfigureAwait(false)としてもUIスレッド例外として捕捉される

さて、そうなると気になるのはawaitで待った後の処理のスレッドに依存するのか、
前の処理のスレッドに依存するのかです。

以下のように待つスレッドを固定しないように書き換えてみました。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(() => 
    {
        string a = null;
        MessageBox.Show(a.ToString());
    }).ConfigureAwait(false); // 元のコンテキストをキャプチャしない
}

すると、
この場合ではUIスレッドの方のイベント( DispatcherUnhandledException )
が発火しました。

awaitで待っていることの方が引き金になっていそうな印象です。

実際には結局どちらもハンドリングすべき

と、いろいろ書き連ねましたが、結局のところ、
UIスレッドの未処理例外もそれ以外のスレッドの未処理例外も、
きちんと捕捉してログを残すなりユーザーに通知するなりすべきです。

どちらかだけ捕捉する必要はほぼありません。
つまり、「この例外はどちらのイベントで捕捉されるのか?」
ということを気にする必要も、ほぼ無いということになります。

備忘のため、書き残しておきますが、
これがアプリ実装の役に立つ機会はそう無いのではないでしょうか。

さんさめ
さんさめ

はまり損かな??

ここまで読んでいただきありがとうございました…。

コメント

タイトルとURLをコピーしました