マルチスレッドによる非同期処理と例外処理

.Net Frameworkでは、スレッドの作成あるいはスレッドプールの利用により、マルチスレッドのプログラミングが比較的容易に行える。スレッド間の情報の授受については、Microsoft社が提供する標準的なドキュメントでその方法がほぼ概観できる。しかし、あるスレッドで発生した例外について、どのようにハンドリングできるのか、あるいはCLRがどのよに処理するのかについては、標準的なドキュメントにはほとんど記載が無い。Microsoft社のサイトに不定期に掲載される記事にいくつか解説を見つけることができるが、言葉使いが不明瞭(標準的なドキュメントには無い用語が多数現われて背景知識が無いと理解が困難)で、実際に実験しないと確証が持てない。

このような事情により、実験した結果を備忘録を兼ねてここに掲げる。

このページではC#で記述し、特に断らない限り、ビルドした実行ファイルを、開発環境によるデバッグ無しで、単独で動作させた場合の挙動を述べる。


新しいスレッドで実行するメソッドで例外が発生する時

次に示すような、1000ミリ秒間待つだけのメソッドを用意し、メソッドの最後で例外を発生させる。

void WaitOneSecond() {
    Thread.Sleep(1000);
    throw new Exception("My exception");
}

このメソッドを、次に示すように、新しいバックグラウンドスレッドで実行する。

Thread th = new Thread(new ThreadStart(WaitOneSecond));
th.IsBackground = true;
th.Start();

WaitOneSecondで発生する例外をtry-catchでハンドリングする有効な方法は無い。

ハンドリングされなかった例外をCLRが扱う動作は.Net Framerowk 1.x以前と2.0以降で異なる。

Windowsアプリケーションで.Net Framework 1.x以前の場合は、例外の発生を知らせる表示は何も出ない。本体のWindowsアプリケーションは動き続ける。.Net Framework 2.0以降の場合は、例外の発生を知らせるダイアログが現われ、これに応答するとWindowsアプリケーション全体が停止する。

コンソールアプリケーションで.Net Framework 1.x以前の場合は、コンソールに例外が発生した旨の表示が現われるが、メソッドを新しいスレッドで呼び出した本体側のスレッドは動き続ける。.Net Framework 2.0以降の場合は、コンソールに例外が発生した旨及びそのスタックトレースの結果が表示され、アプリケーション全体が停止する。

(2010/12/23 16:50)

WaitOneSecondで発生する例外は、.Net Framework 1.x以前でも2.0以降でも、AppDomain.CurrentDomainのUnhandledExceptionイベントで捕まえることができる。ただし、このイベントの直後、.Net Framework 2.0以降の場合は大抵アプリケーション全体が停止するので、例外から回復させることは難しい。このイベントは、例外の記録を残す程度の最後の局面と考えるべきである。.Net Framework 1.x以前の場合はアプリケーションは動作を続ける。


スレッドプールで実行するメソッドで例外が発生する時

上述のメソッドをデリゲートの非同期呼び出しを用いて実行する。

スレッドプールを用いたメソッドの非同期呼び出しの準備

上述のメソッドと同じシグナチャのデリゲート型

delegate void AsyncFunc();

を予め宣言しておき、デリゲートを用意する。

AsyncFunc dlg = new AsyncFunc(WaitOneSec);

その上で、デリゲートの非同期呼び出しをラップして、次のように非同期実行のメソッドのペアを作る。

IAsyncResult BeginWaitOneSec(AsyncCallback callback, object state) {
    return dlg.BeginInvoke(callback, state);
}
void EndWaitOneSec(IAsyncResult ar) {
    dlg.EndInvoke(ar);
}

非同期実行の開始と終了を同じスレッドで呼び出す

IAsyncResult ar = BeginWaitOneSec(null, null);
//他の処理
EndWaitOneSec(ar);

この場合、.Net Framework 1.x以前、2.0以降ともに、EndWaitOneSec()の実行で例外発生が知らされ、呼び出し側のスレッドが停止する。

EndWaitOneSec()をtry-catchの中に置けば、例外を捕捉できる。

コールバックを用いて非同期実行の開始と終了を別のスレッドで呼び出す

次のようなコールバックメソッドを用意する。

void CallBack(IAsyncResult ar) {
    EndWaitOneSec(ar);
}

呼び出し側のスレッドは、

IAsyncResult ar = BeginWaitOneSec(new AsyncCallback(CallBack), null);

だけを記述すればよい。この場合、例外発生はやはりEndWaitOneSec()の実行で知らされるのだが、BeginWaitOneSec()とは異なるスレッド上で実行されていることに注意しなければならない。.Net Framework 1.x以前だと、例外発生は一切知らされず、BeginWaitOneSec()を呼び出したスレッドは動き続ける。.Net Framework 2.0以降だと、例外発生が知らされ、BeginWaitOneSec()を呼び出したスレッドも停止する。

コールバックメソッドの中でEndWaitOneSec()をtry-catchの中に置けば、例外を捕捉できる。

Windowsアプリケーションの場合、コールバックメソッドを実行しているスレッドはUIスレッドとは異なる。このため、コールバックメソッドからUIスレッドで動くイベントを直接呼び出すことや、Formを含むControlの操作を行うことはできない。その代わりに、UIスレッド上で作られたControlのInvokeメソッドを用いる。