非同期処理で起きた例外は通常スルーされる
非同期処理内で発生した未処理(=catchしていない)例外って、
実は気づきにくいのをご存じでしょうか?
例外が起きたらアプリは必ず落ちるって思うじゃろ?
実は、非同期処理で起きた例外って、たとえcatchされていなくても
完全に無視されてしまうんですね。
Microsoftのドキュメントにも「例外が無視されます」とはっきり書いてあります。
サンプルコードで実例を見てみましょう。
以下のコードは、UIスレッド以外のスレッドで例外を発生させます。
xamlのサンプルを置いてないですが、ボタンが1つあるものを想像していただければ。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
string a = null;
// さっきと同じNull参照だけど、そもそもMessageBox表示できんやろ
MessageBox.Show(a.ToString());
});
}
}
さっそく実行してボタンを押してみましょう。
何も起きません。異常終了もしなければ、デバッガも反応しませんね。
例外は起きなかったのでしょうか?
…実は裏で例外は起きています。
「出力」ウィンドウの「出力元」を「デバッグ」にしてみると、
一番下に「例外がスローされました」と表示されているのが確認できます。
(※出力ウィンドウは特にウィンドウの構成を変えてなければ右下にあります。
「無いよ!」という方は、
ツールバーの「デバッグ」→「ウィンドウ」→「出力」で表示できます)
この例から分かるように、
非同期処理で起きた未処理例外は通常無視されます(.Net Framework 4.5以降)
しかし、例外が発生した場合は処理が実行しきっていないので、
意図しない挙動を引き起こすことがあります。
そのため、非同期処理内で起きた例外も開発者としては極力把握しておきたいところです。
そんな時に使えるイベントがあります。
それが、 UnobservedTaskException です。
UnobservedTaskExceptionイベントで購読可能
UnobservedTaskException というイベントを購読すると、
UIスレッド以外の処理、つまり非同期処理中に起きた例外 を捕捉することができます。
試しにUnobservedTaskExceptionイベントを購読してみましょう。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
}
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
MessageBox.Show("申し訳ありません。\n" +
"お使いのアプリケーションは異常を検知したため終了します\n" +
"-- エラー内容 --\n" +
e.Exception.InnerException.ToString(),
"異常終了");
Environment.Exit(1);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
string a = null;
MessageBox.Show(a.ToString());
});
}
}
そして例外発生ボタンを押してみます。
…おや、それでも何も起きませんね。例外ダイアログが出てきません。
実は、このイベントが発火されるタイミングは、
ガベージコレクション(GC)によってTaskクラスが破棄されるときです。
このサンプルコードでは処理がなさ過ぎて、
GCによってTaskクラスが破棄されることがありません。
結果として、何も起きてないように見えてしまいます。
今回は話を分かりやすくするために、強制的にGCを走らせるボタンを追加しましょう。
以下のようなコードになります。
private void Button_Click_1(object sender, RoutedEventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
さて、これで例外発生のボタンを押した後に強制GCを行うボタンを押すと、
イベントが発火され、念願の例外ダイアログが表示されます
UnobservedTaskException の詳しい使い方
UnobservedTaskExceptionでは、渡された引数
UnobservedTaskExceptionEventArgsを使って、
以下のことができます。
- 起きた例外の内容を出力、お知らせする
- アプリケーションを終了させる
1つずつ解説します。
起きた例外の内容を出力・お知らせする
UnobservedTaskExceptionEventArgs.Exceptionプロパティに、
発生した例外が格納されています。
ただし、Taskクラスで起きた例外は、
全てSystem.AggregateExceptionになってしまうため、
InnerExceptionの情報を出力した方が分かりやすいです。
e.Exception.InnerException、ということですね。
前述のサンプルコードでもそうなっていました。
アプリケーションを正常終了させる
イベントを購読してもそのまま流すと、
例外は無視されてアプリケーションは継続されます。
非同期処理自体の目的が情報収集などで、
その処理内で例外が起きてもアプリケーションは終了させなくても良い、
と必ず言えるのであればそれでも良いです。
ただ、例外を無視することは問題の先送りにしかならないことがほとんどです。
一時的に延命することによって被害が拡大する恐れがあります。
そこで、Environment.Exitなどを利用してアプリケーションを終了させてあげます。
Application.Current.Shutdownでも良いのですが、
このイベントはUIスレッドとは別のところから発火するため、
Application.Currentインスタンスにそのままでは アクセスできません。
UIスレッドのDispatcherを介して呼び出せばApplicaion.Current.Shutdownも使えます。
まとめ
まとめです。
- 非同期処理内で起きた例外は通常無視される
- UnobservedTaskExceptionイベントを購読すると無視された捕捉できる
- 例外内容を出力するのが主な用途
ここまでお読みいただき、ありがとうございました。
おまけ
ちなみに…。
非同期処理であるTask.Runをasync/awaitパターンに則り、
以下のように書き換えると、このイベントでは購読できなくなります。
どういうことかというと、UIスレッドの例外として発火するんですね。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
string a = null;
MessageBox.Show(a.ToString());
});
}
ややこしくてさんさめもはまったので、別記事になっています。
【例外】await で待つ処理内で起きた未処理例外はUIスレッドで捕捉される
コメント