追補エントリを書きました。こちらもあわせてご参照ください:
UnhandledExceptionイベントは、サブスレッドの例外も捕捉可能 – SharpLab.

 

先日C# .NETアプリケーション開発 徹底攻略という本を購入しました。C#での業務アプリケーション開発におけるノウハウをまとめた本で、プロファイリングの方法など、普段、趣味グラマをしていると考えない部分について色々と触れているので大変参考になる本なのですが、一点、マルチスレッド処理における例外処理についての記述でおかしなところがありました。

2章6節4項の、サブスレッドで発生した例外の捕捉の仕方についての部分です。まず、93~95ページで、集約例外ハンドラではサブスレッドで発生した例外が捕捉出来ない、という部分があります。このようなコードです。

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Threading;

namespace ThreadExceptionBehavior {
	static class Program {
		/// <summary>
		/// アプリケーションのメイン エントリ ポイントです。
		/// </summary>
		[STAThread]
		static void Main() {

			Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
			Thread.GetDomain().UnhandledException += new UnhandledExceptionEventHandler(Program_UnhandledException);

			Application.EnableVisualStyles();
			Application.SetCompatibleTextRenderingDefault(false);
			Application.Run(new Form1());
		}

		static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) {
			Exception ex = e.ExceptionObject as Exception;
			MessageBox.Show(ex.Message);
		}

		static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) {
			MessageBox.Show(e.Exception.Message);
		}
	}
}

Form1.cs

using System;
using System.Windows.Forms;

namespace ThreadExceptionBehavior {
	public partial class Form1 : Form {
		public Form1() {
			InitializeComponent();
		}

		private delegate void SampleDelegate();

		private void button1_Click(object sender, EventArgs e) {
			//非同期スレッドの実行
			SampleDelegate a = new SampleDelegate(ThreadMethod);
			a.BeginInvoke(null, null);
		}

		private void ThreadMethod() {
			//意図的に起こした例外
			throw new Exception("非同期スレッドで起こった例外です。");
		}
	}
}

ここは良いのです。

そして続いての96~97ページで例外をメインスレッドへの通知して捕捉出来るように書き換える方法が書かれているのですが、このようになっています。

Form1.cs

using System;
using System.Windows.Forms;

namespace ThreadExceptionBehavior {
	public partial class Form1 : Form {
		public Form1() {
			InitializeComponent();
		}

		private delegate void SampleDelegate();

		private void button1_Click(object sender, EventArgs e) {
			//非同期スレッドの実行
			SampleDelegate a = new SampleDelegate(ThreadMethod);
			a.BeginInvoke(null, null);
		}

		private void ThreadMethod() {
			if (this.InvokeRequired) {
				//メインスレッドに呼び戻して実行
				this.BeginInvoke(new SampleDelegate(ThreadMethod));
				return;
			}
			throw new Exception("非同期スレッドで起こった例外です。");
		}
	}
}

19~23行目が加わった部分です。FormのInvokeRequiredプロパティがtrueの場合、つまりメインスレッド(UIスレッド)で実行されていない場合は、非同期にUIスレッドで実行しなおすようになっているのですが、これではサブスレッドでの例外処理の仕方の説明になっていません。ボタンをクリックした後直ぐに処理が帰ってくるという意味では非同期ではありますが、結局UIスレッド上で、ThreadMethodメソッドは動いているわけで、ThreadMethodメソッドに重たい処理を書いた場合は、UIがブロックされてしまうので無意味です。

本来ならば、以下のように書くべきではないでしょうか?

using System;
using System.Windows.Forms;

namespace ThreadExceptionBehavior {
	public partial class Form1 : Form {
		public Form1() {
			InitializeComponent();
		}

		private delegate void SampleDelegate();

		private void button1_Click(object sender, EventArgs e) {
			//非同期スレッドの実行
			SampleDelegate a = new SampleDelegate(ThreadMethod);
			a.BeginInvoke(null, null);
		}

		private void ThreadMethod() {
			try {
	
				//何らかの重たい処理

				throw new Exception("非同期スレッドで起こった例外です。");
			}
			catch(Exception e){
				this.BeginInvoke((MethodInvoker)delegate() {
					throw new ApplicationException("サブスレッドで例外が発生しました。", e);
				});
			}
		}
	}
}

これならば重たい処理によってUIがブロックされることもなく、またサブスレッドで発生した例外も例外集約ハンドラで集めることが可能です。

Comments :5

Z 09-06-27 19:40:13 UTC

ぱっとみ、「どっかおかしいかな?」
って思いましたが、納得です。

確かにこれの方がより良いです。

伊藤 09-07-14 15:38:36 UTC

著者です。
ご意見ありがとうございます。
確かに、おっしゃる通りです。サブスレッドの例外を戻す仕組みという観点で書いてしまったので、実践的な考慮が無かったです。
実際に使う場合は、別スレッドでメインスレッドに影響を与えない処理を行い、その例外をメインスレッドに戻すというのが正しい実装です。この場合、別スレッドの例外処理をどうやったら統一的に処理するかという点が考慮の必要なところですね。(try~catchが乱用されないような仕組み)

改訂の機会があれば参考にさせていただきます。ありがとうございます。

shiroica 09-07-14 21:49:26 UTC

コメントありがとうございます。ご高著大変参考にさせていただいています。
>別スレッドの例外処理をどうやったら統一的に処理するかという点が考慮の必要なところですね。(try~catchが乱用されないような仕組み)
おっしゃる通りだと感じています。上の書き方でマルチスレッド処理を書くと、try~catchがまさに乱用される形になってしまうので、何か代わる上手い方法があれば良いのですが。。

なちゃ 09-09-23 09:15:19 UTC

本来、デリゲートでBeginInvokeした場合は、必ずEndInvokeする必要がありますね。
リソースリークなども発生する危険があります。
※デリゲート以外でもBegin~系は基本的には同じですが

EndInvokeさえすれば、きちんと発生した例外がスローされてきます。
けして例外が握りつぶされているのではなく、正しく使っていないために握りつぶされているように見えているだけですね。
ちょっと書籍の記述がまずいと思います。

shiroica 09-09-23 15:48:49 UTC

ご指摘ありがとうございます。大変参考になりました。
EndInvokeの呼び出し、必要でしたね。。すっかり忘れてました。
Blogのエントリとして書かせて頂きました。
http://blog.sharplab.net/computer/cprograming/3253/

Trackbacks : 3

Trackback URL for this entry
http://blog.sharplab.net/blog/2009/06/02/c-net%e3%82%a2%e3%83%97%e3%83%aa%e3%82%b1%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3%e9%96%8b%e7%99%ba-%e5%be%b9%e5%ba%95%e6%94%bb%e7%95%a5-listing226%e3%81%b8%e3%81%ae%e7%96%91%e5%95%8f/trackback/

Listed below are links to weblogs that reference this entry

ピンバック from 「C# .NETアプリケーシ « P2 - SharpLab. 09-06-02 23:44:44 UTC

[…] プリケーション開発 徹底攻略」という本で自分がおかしい、と思った点をエントリに纏めてみた。間違っていたら突っ込んでください http://blog.sharplab.net/computer/cprograming/3149/   […]

ピンバック from UnhandledExceptionイベントは、サブスレッドの例外も捕捉可能 - SharpLab. 09-09-23 15:35:14 UTC

[…] 以前あげたC# .NETアプリケーション開発 徹底攻略 Listing2.26への疑問 – SharpLab.というエントリに対して、なちゃさんから、 なちゃ 09-09-23 09:15:19 JST […]

トラックバック from Slimming World Extra Easy All In One Reviews 15-07-10 16:17:39 UTC

Slimming World Extra Easy All In One Reviews…

… pharmacy – Similarly, diet pearl white slimming capsule green box are available in the end, of the slimming pills and many more likes. In addition, to concluding halters, bandeau build, he… C# .NETアプリケーション開発 徹底攻略 Listing2.26への疑問 – SharpLab. ……