C# 非同期メソッドを作るにあたり、例外が出るタイミングでハマったメモ

通常のメソッドを非同期化しようとしてハマった罠。
普段 C# を使いこんでる人は既知だろうからスルー推奨。

次のような感じのコードで hoge を非同期化しようとする場合、

void Main() {
  try {
    hoge();
  } catch (Exception e) {
    ※ 例外処理
  }
  ※ 続きの色々な処理
}

void hoge() {
  ※ 例外吐きそうな処理
  ※ 非同期化したい部分
}

こうしないと『※ 例外吐きそうな処理』で生じる例外を拾えない。

void Main() {
  var t = hoge();    // 『※ 例外吐きそうな処理』は同スレッドで走るが、ここで例外は出ない
  if (t.IsFaulted) {
    try {
      t.Wait();      // 『※ 例外吐きそうな処理』が出した例外はここで初めて出てくる
    } catch (Exception e) {
      ※ 例外処理
    }
  }
  ※ 続きの色々な処理
}

async Task hoge() {
  ※ 例外吐きそうな処理
   await Task.Run(() => { ※ 非同期化したい部分 });
}


async - await の説明をざっと見て、『要は await の所まで同じスレッドで走って、そこから先は Worker で走らされて、元スレッドはそのまま戻るんでしょ』と単純に解釈して、 Main から hoge に処理が進んで、『※ 例外吐きそうな処理』が例外出したら catch すればよし。と次のようにしたら、その catch が効かない。

void Main() {
  try {
    hoge();
  } catch (Exception e) {
    ※ 例外処理
  }
  ※ 続きの色々な処理
}

async void hoge() {
  ※ 例外吐きそうな処理
   await Task.Run(() => { ※ 非同期化したい重い部分 });
}


VisualStudio の出力ウィンドウには『例外がスローされました』と出ているし、『※ 例外吐きそうな処理』を try - catch で囲むと間違いなく例外は吐いているので、例外そのものは出ている。

『async void は特殊な状況でしか使わない。async Task にしろ』ということなので Task に変更
するものの、状況は変わらず。

async Task hoge() {
  ※ 例外吐きそうな処理
   await Task.Run(() => { ※ 非同期化したい重い部分 });
}

Task が返ってきているので、ということで、 Wait() をすると、 catch されて『※ 例外処理』が走るものの、『※ 非同期化したい重い部分』を含めて hoge 全体が終わるまでブロックされて、非同期にならない。

void Main() {
  try {
    hoge().Wait();
  } catch (Exception e) {
    ※ 例外処理
  }
  ※ 続きの色々な処理
}

試行錯誤の結果、 Task.IsFaulted プロパティを見れば例外発生の有無が分かると判明し、最初のコードに至る。


async メソッドは、単純に走らせるスレッドを振り分けてるだけじゃなくて、全体が Task でラップされた特殊な環境下になってるんだねってことに気付いた日曜日。
等価コードとかみたら理解進むのかもしれない(自動生成Stateマシンらしい)