プログレスバー
C++のときから考えていたのだが、プログレスバーというのは結構厄介だ。完璧を目指すならば
- 実行すべき重たい処理は別スレッドで実行する
- メインスレッドではプログレスバーを出して待機(Sleep?)しておく
- 別スレッドの重たい処理からメインスレッドのプログレスバーにメッセージを飛ばす(インクリメント)
- プログレスバーのキャンセルが押された場合はメインスレッドからサブスレッドに停止命令を出す
こんな感じになるだろうか。
.NETで実装するなら注意すべきは
- スレッド間でのメッセージのやり取り(特に別スレッドのダイアログメソッドは直接呼べないので注意が必要)。
- 別スレッドで発生した例外をメインスレッドで処理する(メインスレッドでは普通にキャッチできない)
などなど、ちょっと考えただけでも盛りだくさんだし、これが正しいかどうかも良くわからん。とても実装できそうにない。というわけでかなり妥協しながら対応した(でもとてもシンプルに作れたのでこれはこれで結構良い)。
まず元のソース
... ProgressBar.Show() ; //マーキュリー int result = object.Function( arg ) ; ...
Functionが重い処理のためProgressBarがフリーズしてしまう。
でフリーズしないようにしたのがこれ。
... ProgressBar.Show() ; //マーキュリー int result = Hoge.Execute( object.Function, arg ) ; ...
Functionは別スレッドで実行しているため画面はフリーズしない。
例外もcatch可能だ。
うん、とっても簡単。
[追記]コメントにてご指摘をいただいたので勉強
■volatile
volatile キーワードは、同時に実行中の複数のスレッドによってフィールドが変更される可能性があることを示します。 volatile と宣言されているフィールドは、シングル スレッドによるアクセスを前提とする、コンパイラの最適化の対象にはなりません。このため、フィールドには常に最新の値が含まれます。
_isFinishを変更するのはサブスレッド( Executer.Execute() )
_isFinishを判定しているのはメインスレッド( Hoge.Execute() )
なのでvolatileがないとメインスレッドは無限ループする可能性がある
ぐはっ、バグじゃねーか。いまは偶然動いているだけなのか...
■アトミック性
値の変更と読み出しが別スレッドで同時に起きたとしても、読み出される値は、変更前か変更後のどちらかとなり、
変更途中の中途半端な状態の値は得られないこと
なるほど、_isFinishのアトミック性が問題なわけだ。
というわけで_isFinishにvolatileつけときました。
しかしマルチスレッドプログラミングってデバッグ難しいぞ。今回の件だって偶然動いていたわけだし。優秀な卓上デバッガー(人)がいないと超危険な気がする。
以下実装
public static class Hoge{ #region "detail" public delegate ResultT FunctionType(ArgT arg); public class Executer { FunctionType _function = null; ResultT _result; ArgT _arg; volatile bool _isFinish = false; Exception _exception = null; public Executer(FunctionType function, ArgT arg) { _function = function; _arg = arg; } public void Execute() { try { _result = _function.Invoke(_arg); } catch (System.Exception ex) { _exception = ex; } _isFinish = true; } public bool IsFinish { get { return _isFinish; } } public ResultT Result { get { return _result; } } public bool HasException { get { return _exception != null; } } public Exception Exception { get { return _exception; } } } #endregion public static ResultT Execute(FunctionType function, ArgT arg){ Executer exe = new Executer(function, arg); System.Threading.Thread newThread = new System.Threading.Thread(new ThreadStart(exe.Execute)); newThread.IsBackground = true; newThread.Start(); while (!exe.IsFinish){ System.Threading.Thread.Sleep(100); System.Windows.Forms.Application.DoEvents(); } if (exe.HasException) { throw exe.Exception; } return exe.Result; } }