LoginSignup
77

More than 5 years have passed since last update.

async/awaitによる非同期処理の例外の謎

Last updated at Posted at 2015-02-05

await中の非同期処理の例外が拾えない?

@itのサイトでC#でasync/awaitによる非同期処理中に発生した例外処理の勉強をしてまして、なんでもasync/awaitを使用すると通常の同期コードと同じような書き方で例外を拾えるということです。
@itのサイトを参考に以下のようなコードを書いてみました。

        private async void button1_Click(object sender, EventArgs e)
        {
            try
            {
                button1.Enabled = false;
                await Task.Run(() => { System.Threading.Thread.Sleep(1000); throw new Exception("Exception!"); });
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
            finally
            {
                button1.Enabled = true;
            }
        }

解説通りならボタンをクリックしたら1秒後にException!というダイアログが表示されるはず。

しかし実行してみると・・・

unhandled.png

なんでや、工藤!例外拾えてへんやないけ!

@itやその他のasync/awaitの解説サイトを色々みても何故例外を拾えないのか全然わかりませんでした。

非同期実行スレッドをasyncメソッドにすると拾える?

いろいろ試した挙句、以下のようにTask.Runで実行するメソッドに、asyncを追加してみました。

        private async void button1_Click(object sender, EventArgs e)
        {
            try
            {
                button1.Enabled = false;
                await Task.Run(async () => { System.Threading.Thread.Sleep(1000); throw new Exception("Exception!"); });
            }
            catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
            finally
            {
                button1.Enabled = true;
            }
        }

実行してみると・・・

ExceptionDialog.png

上手くいきました。

どうやら非同期実行するメソッドをasyncメソッドとして指定しないと、非同期処理中の例外を拾ってくれないようです。
それ自体はなんとなく分からなくもないんですが、解せないのは@itなどをはじめとした世の中の解説サイトでは、普通にTask.Run(() => ...としていて、それで例外が拾えるとされている事です。
以前はそれで良かったけど、途中でasyncと指定されたメソッドではないとダメというふうに仕様が変わったのでしょうか?PlatformToolsetのバージョンの違いのためでしょうか?
それとも何か根本的な勘違いをしているのでしょうか。謎です。

追記:非デバッグモードだとasyncメソッドじゃなくても拾える?

コメントで、デバッグモードのため拾えていないという指摘を頂きました。
確かに、非デバッグで実行したところ、asyncを付けていないメソッドのTaskをawait中に発生した例外を拾うことが出来ました。
await中のTaskで例外が発生しても通常の同期コードと同じ書き方で例外をキャッチできるのは、Taskを実行しているスレッドで発生した例外を、awaitを呼び出したスレッドに投げなおすようにコンパイラが裏で暗躍してくれるためだと思うのですが、デバッグモードで実行している時に限りasyncメソッドではない通常のメソッドで発生した例外はそのままTaskを実行しているスレッドで投げられるようです。

まとめると、await中のTaskで発生した例外がawait呼出し側のスレッドで発生するか、Task実行スレッドで発生するかは以下の表のようになるようです。

モード 通常メソッド asyncメソッド
デバッグ Task実行スレッド 呼出スレッド
非デバッグ 呼出スレッド 呼出スレッド

なにか理由あっての事だと思いますが、デバッグ時と非デバッグ時で動作が変わるのは通常は好ましくないように思うので、awaitするTaskはやはりasyncメソッドにしておいたほうが無難なように思います。

結論:デバッグモードでも拾えないわけではない

nueccさんに詳細なコメントを頂きました。
詳しく説明してくださっているので、コメントを見てもらうのが良いかと思いますが、結論としては、デバッグモードでもawait中の通常メソッドTaskで発生した例外は拾えます。

  1. デバッグモードでは、本来Task実行スレッドで発生した例外があるという事を伝えるため、一旦そのスレッドで例外を投げる
  2. Task実行スレッド側で例外をキャッチしていなかった場合はユーザコードでハンドルされなかったというダイアログが出る
  3. プログラムを継続させると、改めて呼び出しスレッド側で例外が発生する

という事みたいです。2番めのダイアログが表示された時点で例外をキャッチできなかったと早合点しプログラムを中断してしまっていたのが、誤解の原因となっていました。

警告抑制

asyncメソッドの中で、awaitが一度も出てこないと、コンパイル時に警告が表示されてしまいます。
以下のようにダミーでTask.Yield()あたりをawaitしておくと警告を抑制できます。

    await Task.Run(async () => { await Task.Yield(); System.Threading.Thread.Sleep(1000); throw new Exception("Exception!"); });

asyncメソッドにしないと例外を拾えないと思っていたための苦肉の策だったので、asyncメソッドにする必要がないとわかった今では必要ありません。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
77