トップ回答者
生成したスレッドフォームの終了方法

質問
-
疑問にぶつかっていますのでどなたか教えてください。
メインスレッドからUIスレッドを生成し、生成されたスレッド内でモーダルダイアログを
生成して、バックグラウンドワーカーで排他処理をさせています。
このバックグラウンドワーカー側ではProgress処理内にてBeginInvokeを用いて
メインスレッドのフォームにアクセスしています。
問題はメインフォームを閉じる時、この別スレッドのモーダルダイアログを終了させよ
うとしてBeginInvokeでモーダルダイアログのCloseメソッドを呼び出しているのです
が、その際のモーダル側のOnClosingの処理内でバックグラウンドワーカーを終了さ
せていますが、まれにこのバックグラウンドワーカーが破棄された後にそのバックグラ
ウンドワーカーのProgress処理が走りデバッガでエラーとなってしまいます。
これを防ぐにはどうすればよいでしょうか?
よろしくお願いします。
*メインスレッド側
public partial class Form1 : Form
{
private Thread mThread;
private Form2 Dlg;public Form1()
{
InitializeComponent();
}private void button1_Click( object sender, EventArgs e )
{
mThread = new Thread( DoWork );
mThread.Start();
}private void DoWork()
{
Dlg = new Form2( this );
Dlg.ShowDialog();
}public void Access( string str )
{
label1.Text = str;
}delegate void DlgAccess();
//! フォームが閉じられる時(FormClosingイベント)
private void OnClose( object sender, FormClosingEventArgs e )
{
IAsyncResult ret = BeginInvoke( new DlgAccess( Dlg.Close ) );
EndInvoke( ret );
}
}
*生成スレッド側
public partial class Form2 : Form
{
private int Count = 0;
private Form1 Form;
public Form2( Form1 form )
{
Form = form;
InitializeComponent();
backgroundWorker1.RunWorkerAsync();
}private void DoWork( object sender, DoWorkEventArgs e )
{
while ( backgroundWorker1.CancellationPending == false ) {
backgroundWorker1.ReportProgress( Count++ );
if ( Count >= 1000 ) {
Count = 0;
}
Thread.Sleep( 10 );
}
}delegate void FormAccess( string str );
//! ProgressChangedイベント
private void Progressed( object sender, ProgressChangedEventArgs e )
{
string str = e.ProgressPercentage.ToString();
FormAccess acc = new FormAccess( Form.Access );
IAsyncResult ret = BeginInvoke( acc, str );
EndInvoke( ret );
}//! フォームが閉じられる時(FormClosingイベント)
private void OnClose( object sender, FormClosingEventArgs e )
{
backgroundWorker1.CancelAsync();
while ( backgroundWorker1.IsBusy == true ) {
Application.DoEvents();
}
}- 編集済み shimpo 2010年3月17日 1:44
回答
-
shimpo さん
> 本来実行されるべきでないスレッドでの実行なのに値の表示が出来ているのもそ
> うなると疑問です。
別のスレッドからでも処理可能な操作もあります。
ただし、問題を引き起こす可能性があることは確かですので、そのような箇所は排除しておくべきですね。
> Form1を閉
> じようとした時のOnClosingイベント内のBeginInvoke,EndInvokeの所でForm1
> もForm2も閉じずにそのまま固まってしまうんです。
Form1.OnClose
→ Dlg.Close を BeginInvoke して待機。
待機中はメッセージポンプは止まっている。
Form2.DoWork
→ Form1.Access を BeginInvoke して待機。
キューが処理されないため固まる(Form1.OnClose の待機も)。
ということだと思います。
対処としては、EndInvoke しないのも1つの手だと思います。
(時間ができたので、今から元のコードをじっくり見てみます。)
追記:
<del>私はいろいろ間違ったことを書いてました。
下のコードがありますので、メッセージは処理されますね。
なので、上の固まる理由は間違っていました。
while (backgroundWorker1.IsBusy == true)
{
Application.DoEvents();
}
こちらで試した限りでは、固まることはありませんでした。</del>
ただし他に解ったことや解らないことなどがあり、もう少し調べます。
どなたかからの返信は随時募集中です。(^^; -
以下の2つの理由が関係します。
その1:完了を待つ理由
Form2 では Form1 を参照していますので、Form2 より先に Form1 が終了してしまうことがないようにする必要があります。
そのため、Invoke もしくは BeginInvoke+EndInvoke を使うことで、Form2 の Close の完了を待つ必要があることになります(Close では SendMessage により WM_CLOSE が送られます。もしこれが PostMessage であれば、Close のデリゲートの完了を待っても意味がありません)。
その2:Invoke では固まる理由
完了を待つためには、Invoke するか BeginInvoke+EndInvoke するかのどちらかが必要ですが、待っている間にもメインUIスレッドでのメッセージポンプを行う(Application.DoEvents などを実行する)必要があります。
これは Form2 での backgroundWorker1_ProgressChanged 内でメインUIスレッドへの Invoke の完了を待つ処理があるためです(shimpo さんの以前のスレッドでも触れましたが、BeginInvoke や異なるスレッド間の Invoke では PostMessage が使用されます)。
そのため、メッセージポンプが止まってしまう Invoke は使わずに、BeginInvoke の完了までループし、その中で Application.DoEvents を行う必要があります。
上記が先のコードにした理由ですが、もうひとつ、十分に考察できていないことがありました。
別の方法として、backgroundWorker1_ProgressChanged 内では Invoke の結果を待たない、つまり EndInvoke を行わないようにすると、Form1 の FormClosing での Application.DoEvents は不要にできます。
これが私の先の返信で「EndInvoke しないのも1つの手」と書いたことになります。
ただ、これですと、Form1 への BeginInvoke によるメッセージがたまったまま Form1 が終了した場合に問題にならないか心配になったためこの方法はとらなかったのですが、そもそもメインスレッドが終了すればメッセージキューも破棄されるため、この方法でも問題がないだろうと今は考えています。動作確認の上でも、エラーなく動作します。
ただしこの場合も上記理由その1がありますので、Form1 側の(Application.DoEvents は不要でも)EndInvoke は必要です。(Form1 側でも EndInvoke せずに、Form1 の参照の際に IsDisposed をチェックしてもいいかもしれませんが、別スレッドであるために lock などが必要になり、煩雑になります。)
Form1 側の EndInvoke をやめても動作確認上はエラーが発生しないと思いますが、タイミングが最悪の場合にはエラーになることが十分に想像できます。- 回答としてマーク shimpo 2010年3月19日 5:54
-
> メインスレッドからUIスレッドを生成し、
直接の原因ではないでしょうけど、この部分の実装がどこにもありませんよね?
簡単に UI スレッドとするなら、Form1 の DoWork() の内容を Application.Run(new Form2(this)); とかにすればよいのですが、ShowDialog() だとモーダルループを抜けた後にメッセージポンプが存在しなくなるとか、特定の権限のないユーザで実行できないとか、細かい違いがいくつかあったと思います。
> バックグラウンドワーカーが破棄された後にそのバックグラウンドワーカーのProgress処理が走り
これは具体的にどのような症状を言われていますか? ProgressChanged イベントが発生しようとすることですか? であれば、BackgroundWorker の仕様で、稼働中にキューイングされた ReportProgress() がバックグラウンド処理の停止後に発生することは想定しておく必要があります。 -
一応、こちらで修正したものを書かせていただきます。
// ◆Form1
識別子のネーミングは、検証用としてのなるべく一般的なものに変更しています。
(ネーミングについて K.Takaoka さんが前に書かれていましたが、私もその時に同じことを思ってました。)
(また、複数UIスレッドについては要求仕様などが絡むことで仕方がないのかもしれませんが、これについても私も K.Takaoka さんと同様に思ってます。)
何度か実行しても例外は発生しませんでしたが、マルチスレッドのコーディングミスによる不具合は動作確認ではなかなか検証するのは難しいため、これで問題ないとは言い切れないと思っています(つまり自信がありません(^^;)。
それと、BackgroundWorker が先に破棄される現象は1度きりでその後は再現できなくなり、原因もやはり思いつかないため、対策はできていません。
「CreateHandle() の実行中は・・・」の件も原因を把握できていないため、対策できていません。
このコードでも、shimpo さんの環境で問題が発生しますでしょうか?
public partial class Form1 : Form { private object _syncObj = new object(); private List<Form2> _form2List = new List<Form2>(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // thread はメンバである必要はない。 var thread = new Thread(DoWork); thread.Start(); } private void DoWork() { // 一応、何度 button1 が押されても大丈夫なように List に保持する。 var form2 = new Form2(this); lock (_syncObj) _form2List.Add(form2); form2.ShowDialog(); // ShowMialog(this)としてはいけない lock (_syncObj) _form2List.Remove(form2); form2.Dispose(); // 追加 } public void UpdateForm1Label(string str) { Debug.Assert(!label1.InvokeRequired); // 検証 label1.Text = str; } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { lock (_syncObj) { foreach (var form2 in _form2List) { // BeginInvokeコントロール変更 IAsyncResult ret = form2.BeginInvoke(new MethodInvoker(form2.Close)); while (!ret.IsCompleted) Application.DoEvents(); // 追加 form2.EndInvoke(ret); } } } }
// ◆Form2
public partial class Form2 : Form { public Form2() { InitializeComponent(); } private int _count = 0; private Form1 _form1; public Form2(Form1 form1) { InitializeComponent(); _form1 = form1; backgroundWorker1.WorkerSupportsCancellation = true; backgroundWorker1.WorkerReportsProgress = true; backgroundWorker1.RunWorkerAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { while (backgroundWorker1.CancellationPending == false) { // 頻繁すぎるため高負荷。 backgroundWorker1.ReportProgress(_count++); if (_count >= 1000) _count = 0; Thread.Sleep(10); } } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { string str = e.ProgressPercentage.ToString(); var action = new Action<string>(_form1.UpdateForm1Label); IAsyncResult ret = _form1.BeginInvoke(action, str); // BeginInvokeコントロール変更 // 同期が非常に頻繁に行われるため、別スレッド化している価値は少なくなっているはず。 _form1.EndInvoke(ret); } private void Form2_FormClosing(object sender, FormClosingEventArgs e) { if (backgroundWorker1.IsBusy) // 一応追加 { backgroundWorker1.CancelAsync(); while (backgroundWorker1.IsBusy == true) { // キューの ProgressChanged が処理されるようにする。 Application.DoEvents(); } // CancelAsync によってすぐに IsBusy でなくなったとしても1回は実行する。 Application.DoEvents(); } } }
- 回答としてマーク shimpo 2010年3月18日 23:03
すべての返信
-
> メインスレッドからUIスレッドを生成し、
直接の原因ではないでしょうけど、この部分の実装がどこにもありませんよね?
簡単に UI スレッドとするなら、Form1 の DoWork() の内容を Application.Run(new Form2(this)); とかにすればよいのですが、ShowDialog() だとモーダルループを抜けた後にメッセージポンプが存在しなくなるとか、特定の権限のないユーザで実行できないとか、細かい違いがいくつかあったと思います。
> バックグラウンドワーカーが破棄された後にそのバックグラウンドワーカーのProgress処理が走り
これは具体的にどのような症状を言われていますか? ProgressChanged イベントが発生しようとすることですか? であれば、BackgroundWorker の仕様で、稼働中にキューイングされた ReportProgress() がバックグラウンド処理の停止後に発生することは想定しておく必要があります。 -
>直接の原因ではないでしょうけど、この部分の実装がどこにもありませんよね?
失礼しました。通常のスレッドを生成してスレッド処理本体であるDoWork()でモーダル
ダイアログを生成しています。
>ReportProgress() がバックグラウンド処理の停止後に発生することは想定しておく必要があります。
仕様なのですか。バックグラウンドワーカーが破棄されるのはProgressChangeイベント
あればそれが終了した後だと思っていました。
するとForm2のFormClosingイベント内でバックグラウンドワーカー終了してからも
ProgressChangeが残っているかどうかを知る必要があり、あれば終了するまで
FormClosingイベントから抜けてはいけないという事になるのでしょうか。
ProgressChangeがあるかどうか知る方法ってどうすればよいのでしょうか。
まだ.NETを始めて1年にもならないので壁だらけです。 -
> ProgressChangeがあるかどうか知る方法ってどうすればよいのでしょうか。
こちらは不明ですが、「ProgressChanged イベント」が発生することと「ProgressChanged イベントハンドラ」が呼び出されることは別なので、「ProgressChanged イベント」から「ProgressChanged イベントハンドラ」を取り除くことで、利用者的には解決ができる場合があります。
具体的には、前述の例であれば、backgroundWorker1.ProgressChanged -= Progressed; と、ProgressChanged イベントから Progressed イベントハンドラを削除することになります。
イベントハンドラが呼ばれてしまうことが問題なのであれば、これで解決するのではないかと思います。 -
Form2のFormClosingイベントでCancelAsync前にProgressed イベントハンド
ラを削除しても既にポストされた分があるようで同じ例外(Form1へのアクセス)
が発生する時があります。やはりProgressChangeがあるかどうかを見ないと
いけないのでしょうか。
後、DoWork()のForm2生成を仰るようにApplication.Run()に変えた所、
Form1のFormClosingイベントからBeginInvokeで呼び出しているForm2の
CloseメソッドでもForm2が閉じず残ったままになる時があります。
更に、Form2が閉じてもデザイナの所で「CreateHandle() の実行中は値
Dispose() を呼び出せません。」というエラーが発生する時があります。
問題がいくつもあるようで何が起因なのかわかりません。
根本的にやり方が間違っているのでしょうか。- 編集済み shimpo 2010年3月18日 0:48
-
とりあえず、
・目的(やりたいこと)
・手段(自分で考えた、やりたいことを実現する方法)
・結果(どこで、なにが、どのような問題を発生させているか)
を、きちんとまとめてみてはどうでしょうか?
「同じ例外(Form1へのアクセス)」では、読んでいる人には何も伝わりません。ですので、前にも「にどのような症状を言われていますか? ProgressChanged イベントが発生しようとすることですか?」と聞いています。
全体的に気になるところは、
・サンプルコードなのか、実際に問題になっているコードなのか
たとえば、BeginInvoke() を利用されていますが、実際に問題となっているコードでは BeginInvoke() と EndInvoke() の間に処理があるため Invoke() が使えないのか、単純に使い分けかたがわからないとか、挙動をチェックしているとかといった学習的な目的で BeginInvoke() になっているのか、といったことが伝わってこないです。
・イベントハンドラの名称が Visual Studio 等の自動生成と異なる点
多くの人に見てもらって返信が貰いたい場合、できれば直しておいたほうが見てくれる人が増え、有用な助言を得やすくなるでしょう。
・環境について書いておきましょう
Visual Studio を利用してれば、そのバージョン。ターゲットとしている .NET Framework のバージョンなんかも書いておいたほうがいいと思います。特に delegate まわりは .NET や C# のバージョンによって書き方が色々可能だったりもします。 -
目的、手段は一番最初の投稿に書いたとおりです。
繰り返しになりますが、Form1からスレッドを生成してそのスレッド内でForm2を生成し
ます。Form2ではバックグラウンドワーカーを用いてProgressChangedイベント(ソー
ス中のProgressed())から単にカウンタ値を更新してその値をForm1に表示している
だけです。この状態がRun状態です。
今、起きている問題ですが、Form1を閉じる時にForm2を一緒に閉じたいので、Form1
のFormClosingイベントでForm2のCloseメソッドを呼び出してForm2を閉じさせようとし
ているのですが、以下のような例外が発生しているという事です。
・CreateHandle() の実行中は値 Dispose() を呼び出せません。という例外がForm1
のデザイナの所でまれに出る
・コントロールが作成されたスレッド以外のスレッドからコントロール'Form1' がアクセスさ
れた。という例外が同じくForm1のデザイナの所でまれに出る
環境についてはここはVisual C# Express Editionのフォーラムですので、勿論同じ
です。バージョンは2008です。
.NETの対象バージョンは2008ExpressEditionのデフォルトの3.5のままとしています。
BeginInvokeは別スレッド間のフォームへのアクセスにおいてはデリゲートを使わないと
アクセスできないという内容を記載したサイトを見て、そこでBeginInvokeを使う例があっ
たのでそれを引用しています。- 編集済み shimpo 2010年3月18日 0:48
-
まず、BeginInvoke() ですが、通常は Invoke() で良いはずです。
Invoke() と BeginInvoke() の違いですが、たとえば Form1 から Form2 の Close() メソッドを Invoke() で呼び出すと、Invoke() は Form2 の Close() メソッドが完了するのを待機し、Close() メソッドが完了するまで処理を停止します。
それに対し、BeginInvoke() を呼び出した場合、Close() メソッドが完了していない状態( Form2 がまだ開いている状態) であっても、BeginInvoke() メソッドは処理を完了して次へ進みます。対応する EndInvoke() は Invoke() と同様に BeginInvoke() で呼び出したデリゲートが完了するのを待機します。このような感じで、BeginInvoke() は呼び出したデリゲートが完了するのを待たず、他の作業を行いたいときに利用します。
スレッドを生成するのが目的なのか、バックグラウンドの作業の作業経過を画面上に表示するのが目的なのか、というのは大きな違いです、という話です。
以下は、バックグラウンドの作業を画面上に表示するだけであれば、、、という話になります。
全体に対して一番大きな影響を与えているものとしては、Form2 を別スレッドで生成することだろうと思います。これは目的に必要なことなのでしょうか? どのような目的があって Form2 を別スレッドとしているのか、まったく話にあがっていないため、それがなんらかの目的によっているものなのか、BeginInvoke のような学習課程による選択なのか、見ている人には判断しようがないのですね。
通常の Windows Forms を利用したアプリケーションの場合、UI スレッドは1つだけです。この状態を変更するのはかなり大掛かりなもので、 Windows のメッセージ関連の詳細と、Windows Forms のライブラリの詳細をかなり掘り下げて調べるぐらいの根性がないと、一見して不可解な問題にあたったときに無理がでます。・Form2ではバックグラウンドワーカーを用いて単にカウンタ値を更新してその値をForm1に表示している
・Form1を閉じる時にForm2を一緒に閉じたいぐらいのことであれば、Form2 を別 UI スレッドで起動する必要性はないと思います。
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private Form2 Dialog; private void button1_Click(object sender, EventArgs e) { // Form2 を生成し、Owner を Form1 にする // * Owner が Form1 なので、ダイアログと同様に Form2 は Form1 より前面に表示されます // * Owner が Form1 なので、Form1 を閉じると Form2 も閉じられます this.Dialog = new Form2(this); this.Dialog.Show(this); } public void Access(string text) { this.textBox1.Text = text; } } public partial class Form2 : Form { private Form2() { InitializeComponent(); } public Form2(Form1 form1) : this() { this.Form = form1; this.backgroundWorker1.RunWorkerAsync(); } private int Count = 0; private Form1 Form; private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // キャンセルするまで処理を継続する while (!this.backgroundWorker1.CancellationPending) { this.backgroundWorker1.ReportProgress(this.Count++); if (this.Count >= 1000) this.Count = 0; Thread.Sleep(100); } // DoWork イベントがキャンセルで終了した e.Cancel = true; } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { // Form1.Access() を呼んでカウントを表示 this.Form.Invoke(new Action<string>(this.Form.Access), this.Count.ToString()); } private void Form2_FormClosing(object sender, FormClosingEventArgs e) { if (this.backgroundWorker1.IsBusy) { this.backgroundWorker1.CancelAsync(); while (this.backgroundWorker1.IsBusy) Application.DoEvents(); } } }
CancelAsync() の後、Application.DoEvents() を呼び出してもよいのですが、 private void Form2_FormClosing(object sender, FormClosingEventArgs e) { // バックグラウンドで作業中に閉じようとした場合、 // バックグラウンドの作業も、閉じるも、共にキャンセルする if (this.backgroundWorker1.IsBusy) { this.backgroundWorker1.CancelAsync(); e.Cancel = true; } } と、フォームを閉じようとした際に、いったん閉じるのをキャンセルしておいて、 private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // バックグラウンドの作業が完了した時、キャンセルによる場合はフォームを閉じる if (e.Canceled) this.Form.Close(); } と、バックグラウンドワーカーが作業を終了した後で、あらためてフォームを自動的に閉じるというような対応でも、同じ効果が得られます。
-
Invokeについては理解できました。ありがとうございました。
C#, .NET共に初心者な為、疑問が山積みです。
MFCなら何年かやっていたのでモードレスダイアログを用いれば、単にバック
グラウンドの作業を画面上に表示するだけであれば別スレッドにしなくても出
来るのはわかっています。
Form2を別スレッドにしているのは現在の作成プログラムの仕様上からです。
実際に作成しているコードはもっといろんな処理が入っていてここに挙げれな
いので同じ問題が再現するスケルトン的な物を作成してここにそのソースを挙
げた訳です。
まだあれこれやっていますが、どうもProgressChangedイベント内の
FormAccess acc = new FormAccess( Form.Access );
IAsyncResult ret = BeginInvoke( acc, str );
EndInvoke( ret );
この部分がまずいみたいです。この3行を抜くと(当然値は表示されませんが)
Form1,Form2共に正常に終了します。
親インスタンス側の処理をInvokeすると何か不都合が起きるのでしょうか。
試しにInvokeにせずにPostMessageにしてForm1側のWndProcで受けた
メッセージのパラメータとして値を受け取り表示・・というやり方ではForm1も
Form2も正常に終了します。なぜかInvokeにするとだめみたいです。 -
shimpo さん
> 別スレッド間のフォームへのアクセスにおいてはデリゲートを使わないと
ここについて、1点、補足させていただきます。
(K.Takaoka さんのコードでは正しく書かれていますが、文章内では指摘されていませんので。)
Control.BeginInvoke は、その Control のスレッドにてデリゲートが実行されます。
shimpo さんの書かれたコードでは、Control が this となっていますので、これにより(本来処理すべきスレッドではない)this のスレッドでの、たんなる非同期実行(EndInvoke があるのでほぼ同期実行)の扱いになります。
つまり、Invoke する目的が満たされないまま、直接実行しているのと同じになってしまっています。 -
shimpo さん
> 本来実行されるべきでないスレッドでの実行なのに値の表示が出来ているのもそ
> うなると疑問です。
別のスレッドからでも処理可能な操作もあります。
ただし、問題を引き起こす可能性があることは確かですので、そのような箇所は排除しておくべきですね。
> Form1を閉
> じようとした時のOnClosingイベント内のBeginInvoke,EndInvokeの所でForm1
> もForm2も閉じずにそのまま固まってしまうんです。
Form1.OnClose
→ Dlg.Close を BeginInvoke して待機。
待機中はメッセージポンプは止まっている。
Form2.DoWork
→ Form1.Access を BeginInvoke して待機。
キューが処理されないため固まる(Form1.OnClose の待機も)。
ということだと思います。
対処としては、EndInvoke しないのも1つの手だと思います。
(時間ができたので、今から元のコードをじっくり見てみます。)
追記:
<del>私はいろいろ間違ったことを書いてました。
下のコードがありますので、メッセージは処理されますね。
なので、上の固まる理由は間違っていました。
while (backgroundWorker1.IsBusy == true)
{
Application.DoEvents();
}
こちらで試した限りでは、固まることはありませんでした。</del>
ただし他に解ったことや解らないことなどがあり、もう少し調べます。
どなたかからの返信は随時募集中です。(^^; -
shimpo さん
> 本来実行されるべきでないスレッドでの実行なのに値の表示が出来ているのもそ
> うなると疑問です。
この件、理由がわかりました。
まず先に Control.Invoke の場合ですが、Control のスレッドが別であっても、InvalidOperationException(作成されたスレッド以外からのアクセス)は発生しないようにフレームワーク内で制御されていました(MultithreadSafeCallScope 内ではスレッドのチェックが迂回されるようになっていました)。
次に Control.BeginInvoke ですが、Control.EndInvoke までにデリゲートが実行されていない場合(今回の場合)は EndInvoke の時点でデリゲートが実行されますが、EndInvoke 内でも Invoke と同様に、スレッドのチェックが迂回されるようになっていました。もし EndInvoke までにデリゲートが実行された場合は、正しく例外が発生します。
ただし、上記のどちらにおいても、例外が発生しないために問題がないというわけではなく、コントロールはスレッドセーフではないため、複数のスレッドからの操作時には不可解な結果になりえます。にもかかわらずにチェックが迂回されるようになっているのは、おそらく、UIスレッド自体が複数存在することまでは面倒を見ていないからだろうと想像します。スレッドをチェックして例外を発生させているのはイージーミスを防ぐことが目的だと思いますので、例外が正しく発生しない場合があってもフレームワークとしては致命的な問題ではないと考えます。
それと、デバッガがアタッチされていない場合にもスレッドのチェックは迂回されるようになっていました。イージーミスを防ぐという目的やパフォーマンスを考えると、これも妥当かと思いました。
例外が発生するかどうかとは別に、正しく呼び出せているかどうかについては、Form1.Access メソッド内で
label1.InvokeRequired
の値を確認すると解ります。true が返される場合は、正しく呼び出せていない箇所があることになります。
Invoke について長々となりましたが、少なくとも shimpo さんの元コードのような BeginInvoke には問題があり、それが今回の問題につながっていた可能性があります。
次に、残りの問題点についてです。
> ・CreateHandle() の実行中は値 Dispose() を呼び出せません。という例外がForm1
> のデザイナの所でまれに出る
これですが、これは今回最初に提示されたミニマムコードでも発生する現象でしょうか?
それとも実際のコードの方の話でしょうか?
今回のコードでは、この現象につながる箇所は見つけられませんでした。
> まれにこのバックグラウンドワーカーが破棄された後にそのバックグラ
> ウンドワーカーのProgress処理が走りデバッガでエラーとなってしまいます。
と、
> Form1を閉
> じようとした時のOnClosingイベント内のBeginInvoke,EndInvokeの所でForm1
> もForm2も閉じずにそのまま固まってしまうんです。
については、こちらでも現象を確認でき、理由もおおよそ解りました。(やはり1つ前の返信内容が関係していました。)
ただ、まだ十分な説明や対策が示せず、とりあえず今日はここまでにさせていただきます。 -
>Invoke について長々となりましたが、少なくとも shimpo さんの元コードのような
>BeginInvoke には問題があり、それが今回の問題につながっていた可能性があります。
これはProgressChangedイベント内のコードの事でしょうか?
試しにBeginInvokeではなくInvokeにしていますが、現象は同じです。
Form1のFormClosingイベントから呼び出すInvoke( Dlg.Close )で固まり、
その時はForm2のFormClosingイベントにも飛んできません。
>これですが、これは今回最初に提示されたミニマムコードでも発生する現象でしょうか?
>それとも実際のコードの方の話でしょうか?
ミニマムコードでも発生しています。
どうもInvokeの動きが掴めないです。MSDNを何度も読み直してはいますが
相変わらず理解困難です。
PostMessage+WndProcでは動作するので逃げ策はあるとしても後味が悪い
のでInvokeで動作するようにしたいです。- 編集済み shimpo 2010年3月18日 0:43
-
shinmpo さん
> これはProgressChangedイベント内のコードの事でしょうか?
> 試しにBeginInvokeではなくInvokeにしていますが、現象は同じです。
BeginInvoke や Invoke の箇所すべてに関する話です。
これは固まる件とは別に、何らかの不具合を招く可能性を排除するための修正になります。
> ミニマムコードでも発生しています。
そうですか…。
発生するのはどのような操作時でしょうか?
> Form1のOnClosingイベントから呼び出すInvoke( Dlg.Close )で固まり、
> その時はForm2のOnClosingイベントにも飛んできません。
固まるのは EndInvoke の箇所ではないでしょうか?
この件、とりあえず Form1 の FormClosing を以下のようにすれば回避できます。
理由は2つ前の私の返信の通りでしたが、時間ができたときにもう少し調べたいことがあります。private void OnClose( object sender, FormClosingEventArgs e )
{
IAsyncResult ret = BeginInvoke( new DlgAccess( Dlg.Close ) );
while (!ret.IsCompleted) Application.DoEvents(); // 追加
EndInvoke( ret );
}
追記:それと一応以下の修正もしてください。
Dlg.ShowDialog();
の後に
Dlg.Dispose();
を追加し、
Form1 の OnClose の先頭に
if (Dlg == null || Dlg.IsDisposed) return;
を追加してください。- 編集済み TH01 2010年3月18日 0:39 追記
-
>Form1.OnClose
>→ Dlg.Close を BeginInvoke して待機。
> 待機中はメッセージポンプは止まっている。
>
>Form2.DoWork
>→ Form1.Access を BeginInvoke して待機。
> キューが処理されないため固まる(Form1.OnClose の待機も)。
>
上記の事を考えると
・Form2ではProgressChangedにより常にForm1のメッセージポンプ
を使用して待機というのを繰り返している。
・その時にForm1のFormClosingイベントでForm2.Invokeをしてしまうと
Form1のメッセージポンプが待機になってしまう為、Form2のProgressChanged
が固まる
・Form2が固まる為、当然Form2のInvoke完了待機になっているForm1も固まる
という事になるのでしょうか。
それを前提にしてForm1のFormClosingイベントで直接以下のようにして
みたら正常にForm1もForm2も終了するようになりました。
Dlg.backgroundWorker1.CancelAsync();
while ( Dlg.backgroundWorker1.IsBusy == true ) {
Application.DoEvents();
}
Dlg.Invoke( new DlgAccess( Dlg.Close ) );
Form2側のOnClosingイベントのバックグラウンドワーカー終了処理も
Form2を直接閉じようとする場合に必要なので残しておく必要がありま
すね。処理がダブりますけど。
なんだか釈然としないような・・・。 -
> それを前提にしてForm1のFormClosingイベントで直接以下のようにして
> みたら正常にForm1もForm2も終了するようになりました。
昨日私もそれを考えたのですが、Form1 のスレッドから Form2 のスレッドが所有する BackGroundWorker を操作するのもちゃんと調べてからでなければ危険だと考えました。
BackGroundWorker がどんな Windows のハンドルを持っているかや、誰に所有されているのかについても、調べようと思っています(普通の Component に比べると特殊に思います)。
ただ、それに比べれば、1つ前の私の返信に書いた対策の方が安全だと考えました。 -
> モードレスダイアログを用いれば、単にバックグラウンドの作業を画面上に表示するだけで
> あれば別スレッドにしなくても出来るのはわかっています。
いえ、違います。モードレスであってもモーダルであっても、可能です。
前回のサンプルとしてモードレスにした理由は、Form2 を表示したまま Form1 を閉じる必要があるためです。これがモーダルであろうとモードレスであろうと、UI スレッドを増やす理由にはなりえません。
実際の問題として UI スレッドが複数必要になることは、非常に稀有なケースです。ほとんどの場合で非 UI スレッドを増やすだけで解決します。
# UI スレッドを1つにまとめる、というのが最良の選択肢ではあると思いますが
# このあたりは設計等の別問題なので、このあたりでおいておきましょう -
一応、こちらで修正したものを書かせていただきます。
// ◆Form1
識別子のネーミングは、検証用としてのなるべく一般的なものに変更しています。
(ネーミングについて K.Takaoka さんが前に書かれていましたが、私もその時に同じことを思ってました。)
(また、複数UIスレッドについては要求仕様などが絡むことで仕方がないのかもしれませんが、これについても私も K.Takaoka さんと同様に思ってます。)
何度か実行しても例外は発生しませんでしたが、マルチスレッドのコーディングミスによる不具合は動作確認ではなかなか検証するのは難しいため、これで問題ないとは言い切れないと思っています(つまり自信がありません(^^;)。
それと、BackgroundWorker が先に破棄される現象は1度きりでその後は再現できなくなり、原因もやはり思いつかないため、対策はできていません。
「CreateHandle() の実行中は・・・」の件も原因を把握できていないため、対策できていません。
このコードでも、shimpo さんの環境で問題が発生しますでしょうか?
public partial class Form1 : Form { private object _syncObj = new object(); private List<Form2> _form2List = new List<Form2>(); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // thread はメンバである必要はない。 var thread = new Thread(DoWork); thread.Start(); } private void DoWork() { // 一応、何度 button1 が押されても大丈夫なように List に保持する。 var form2 = new Form2(this); lock (_syncObj) _form2List.Add(form2); form2.ShowDialog(); // ShowMialog(this)としてはいけない lock (_syncObj) _form2List.Remove(form2); form2.Dispose(); // 追加 } public void UpdateForm1Label(string str) { Debug.Assert(!label1.InvokeRequired); // 検証 label1.Text = str; } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { lock (_syncObj) { foreach (var form2 in _form2List) { // BeginInvokeコントロール変更 IAsyncResult ret = form2.BeginInvoke(new MethodInvoker(form2.Close)); while (!ret.IsCompleted) Application.DoEvents(); // 追加 form2.EndInvoke(ret); } } } }
// ◆Form2
public partial class Form2 : Form { public Form2() { InitializeComponent(); } private int _count = 0; private Form1 _form1; public Form2(Form1 form1) { InitializeComponent(); _form1 = form1; backgroundWorker1.WorkerSupportsCancellation = true; backgroundWorker1.WorkerReportsProgress = true; backgroundWorker1.RunWorkerAsync(); } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { while (backgroundWorker1.CancellationPending == false) { // 頻繁すぎるため高負荷。 backgroundWorker1.ReportProgress(_count++); if (_count >= 1000) _count = 0; Thread.Sleep(10); } } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { string str = e.ProgressPercentage.ToString(); var action = new Action<string>(_form1.UpdateForm1Label); IAsyncResult ret = _form1.BeginInvoke(action, str); // BeginInvokeコントロール変更 // 同期が非常に頻繁に行われるため、別スレッド化している価値は少なくなっているはず。 _form1.EndInvoke(ret); } private void Form2_FormClosing(object sender, FormClosingEventArgs e) { if (backgroundWorker1.IsBusy) // 一応追加 { backgroundWorker1.CancelAsync(); while (backgroundWorker1.IsBusy == true) { // キューの ProgressChanged が処理されるようにする。 Application.DoEvents(); } // CancelAsync によってすぐに IsBusy でなくなったとしても1回は実行する。 Application.DoEvents(); } } }
- 回答としてマーク shimpo 2010年3月18日 23:03
-
> // 同期が非常に頻繁に行われるため、別スレッド化している価値は少なくなっているはず。
BackgroundWorker は不特定多数のスレッドを消費するような仕組みになっていて
[main thread (UI)]
[worker thread (BackgroundWorker.DoWork) ]
[report thread (BackgroundWorker.ProgressChanged)]
[report thread (BackgroundWorker.ProgressChanged)]
[report thread (BackgroundWorker.ProgressChanged)]
[report thread (BackgroundWorker.ProgressChanged)]
[report thread (BackgroundWorker.ProgressChanged)]
みたいなかんじで、(ThreadPool の上限まで...デフォルトは小さいですが...) 大量にスレッドが生成され、ProgressChanged イベントの発生順序は順不同になるんじゃなかったかな~。
# それが非常に便利で、そして使いにくい…なので、私は BackgroundWorker はテストコード以外では使わない^^ -
挙げていただいたコードでは大丈夫でした。
ただ、C#初心者の私には難しい部分が幾つかあるコードです。。。
後、Debugは現在のコンテキストに存在しません。と言われますので
とりあえずコメントアウトして実行しました。
ただ、BeginInvoke.EndInvokeではなくInvokeだと固まりました。
(ますますInvokeの理屈がわかりません)
>(ネーミングについて K.Takaoka さんが前に書かれていましたが、私もその時に同じことを思ってました。)
お聞きしたいのですが、何をもって標準的といわれるのかがわかりません。
フォームのプロパティウィンドウのイベントを開くと各イベント名が出てきてその
右側にハンドラ名を入れる訳ですが、そこには元々ハンドラ名が表示されてい
る訳ではなく空欄で、ユーザーが任意に入れる訳ですよね?
なのに標準的な名前と言われても、何が基準なのかわかりません。- 編集済み shimpo 2010年3月18日 7:21
-
横からすみません。
> お聞きしたいのですが、何をもって標準的といわれるのかがわかりません。
> フォームのプロパティウィンドウのイベントを開くと各イベント名が出てきてその右側にハンドラ名を入れる訳ですが、
> そこには元々ハンドラ名が表示されている訳ではなく空欄で、ユーザーが任意に入れる訳ですよね?
> なのに標準的な名前と言われても、何が基準なのかわかりません。
プロパティウィンドウのイベント名の右側の空欄をダブルクリックすると、
「コントロール名_イベント名」 で、自動的にコードが生成されます。
ひらぽん http://blogs.yahoo.co.jp/hilapon/ -
shimpo さん
> ただ、C#初心者の私には難しい部分が幾つかあるコードです。。。
相違点が問題を引き起こしている箇所の可能性がありますので、じっくり見ていただければと思います。
ただ、私にも怪しいところがあるので、解っている人が私のコードに対してツッコミを入れてもらえるとうれしいです。
K.Takaoka さんの「ProgressChanged イベントの発生順序は順不同になるんじゃなかったかな~」が、ツッコミなのか補足なのか判断に迷ってます。(^^;
List<Form2> にしたのは、もし子画面の表示ボタンが何度も押された場合に対処するもので、shimpo さんの検証に必要かどうかはわからないまま追加しています。余分かもしれません。
> 右側にハンドラ名を入れる訳ですが、そこには元々ハンドラ名が表示されてい
> る訳ではなく空欄で、ユーザーが任意に入れる訳ですよね?
> なのに標準的な名前と言われても、何が基準なのかわかりません。
そういうことですか。
私は逆に、なぜ標準的な名前から変更されているのだろうと、不思議に思っていました。(^^;
Express エディションでも同じか解りませんが、別の方法としてソースエディタ上で
button1.Click +=
まで入力してタブを2回押してみてください。
その場合も同様のネーミングルールでハンドラが作成されます。
この識別子のままにするか変更するかはもちろん任意ですし、本来は解りやすい名前に変更すべきなのですが、不特定の人への検証用コードの場合には標準のままにしておいた方がソースの内容を伝えやすくなると思っています。
長い名前には、そのうち慣れると思いますよ。
後、Form という名前は System.Windows.Forms.Form クラスともダブるため、別の人が見るときには混乱を招く可能性があります。検証用のコードではどのフォームかもぱっと見て解るように、form1 のような素直な名前の方がよいと思います。(あくまで不特定への検証用コードの話になります。)
それと、ソースコードの補足として書き忘れたことですが、私のコードでは汎用のデリゲートを使用しています。これはたんなる好みでして、本当は shimpo さんのように専用のデリゲートを定義した方が、一般的にはよいとされています。 -
以下の2つの理由が関係します。
その1:完了を待つ理由
Form2 では Form1 を参照していますので、Form2 より先に Form1 が終了してしまうことがないようにする必要があります。
そのため、Invoke もしくは BeginInvoke+EndInvoke を使うことで、Form2 の Close の完了を待つ必要があることになります(Close では SendMessage により WM_CLOSE が送られます。もしこれが PostMessage であれば、Close のデリゲートの完了を待っても意味がありません)。
その2:Invoke では固まる理由
完了を待つためには、Invoke するか BeginInvoke+EndInvoke するかのどちらかが必要ですが、待っている間にもメインUIスレッドでのメッセージポンプを行う(Application.DoEvents などを実行する)必要があります。
これは Form2 での backgroundWorker1_ProgressChanged 内でメインUIスレッドへの Invoke の完了を待つ処理があるためです(shimpo さんの以前のスレッドでも触れましたが、BeginInvoke や異なるスレッド間の Invoke では PostMessage が使用されます)。
そのため、メッセージポンプが止まってしまう Invoke は使わずに、BeginInvoke の完了までループし、その中で Application.DoEvents を行う必要があります。
上記が先のコードにした理由ですが、もうひとつ、十分に考察できていないことがありました。
別の方法として、backgroundWorker1_ProgressChanged 内では Invoke の結果を待たない、つまり EndInvoke を行わないようにすると、Form1 の FormClosing での Application.DoEvents は不要にできます。
これが私の先の返信で「EndInvoke しないのも1つの手」と書いたことになります。
ただ、これですと、Form1 への BeginInvoke によるメッセージがたまったまま Form1 が終了した場合に問題にならないか心配になったためこの方法はとらなかったのですが、そもそもメインスレッドが終了すればメッセージキューも破棄されるため、この方法でも問題がないだろうと今は考えています。動作確認の上でも、エラーなく動作します。
ただしこの場合も上記理由その1がありますので、Form1 側の(Application.DoEvents は不要でも)EndInvoke は必要です。(Form1 側でも EndInvoke せずに、Form1 の参照の際に IsDisposed をチェックしてもいいかもしれませんが、別スレッドであるために lock などが必要になり、煩雑になります。)
Form1 側の EndInvoke をやめても動作確認上はエラーが発生しないと思いますが、タイミングが最悪の場合にはエラーになることが十分に想像できます。- 回答としてマーク shimpo 2010年3月19日 5:54
-
仕様的な問題は、Control.Invoke() 系が、「作業スレッドから UI スレッドを呼び出す」のが目的で設計されていて、その背景には「UI スレッドは Busy にならず、常にユーザの要求を受け入れ、反映するために稼働しつづける」という Win32 のウィンドウアプリケーションでは当たり前の状況が想定されているんですね。
真面目に複数の UI スレッドにて処理を行うことを考えると、この条件を双方の UI スレッドに対して維持しなければならないでしょう。以下は、using WinForms; を行うと、Invoke() の替わりに使える InvokeAction() を提供するという形のイメージ・・・文法チェックのみの手書きなので動作未確認。
# こういうオーバーロード時に T4 が使えないのは Express Edition の欠点ですね...using System; using System.Threading; using System.Windows.Forms; namespace WinForms { /// <summary>for System.Windows.Forms.Control</summary> public static class ControlExtension { /// <summary> /// UI スレッドが待機状態になった際に、呼び出されるメソッド /// <para>未設定の場合、<see cref="Application.DoEvent()"/> が呼び出されます。</para> /// </summary> [ThreadStatic] public static Action WaitCallback; /// <summary>コントロールの基になるウィンドウ ハンドルを所有するスレッド上で、指定したアクションを実行します。</summary> /// <param name="control">スレッドを特定するためのウィンドウ ハンドルを所有するコントロール</param> /// <param name="action">コントロールのスレッド コンテキストで呼び出されるメソッドを格納しているアクション。</param> public static void InvokeAction(this Control control, Action action) { if (!control.InvokeRequired) { // 直接呼び出し可能なので直接呼び出す action(); } else if (!Application.MessageLoop) { // 作業スレッドから UI スレッドを呼び出す control.Invoke(action); } else { // UI スレッドから、UI スレッド // 作業スレッドを作成し、メソッドを呼び出しを代行させる var invokeWorker = new Thread(() => InvokeAction(control, action)) { Name = string.Format("InvokeWorker({0})", Thread.CurrentThread.ManagedThreadId), IsBackground = true, }; // 自身は、別の UI スレッドからの要求に応えらえるように待機する invokeWorker.Start(); while (!invokeWorker.Join(0)) (WaitCallback ?? Application.DoEvents)(); } } public static void InvokeAction<TArg>(this Control control, Action<TArg> action, TArg arg) { control.InvokeAction(() => action(arg)); } public static void InvokeAction<TArg1, TArg2>(this Control control, Action<TArg1, TArg2> action, TArg1 arg1, TArg2 arg2) { control.InvokeAction(() => action(arg1, arg2)); } public static void InvokeAction<TArg1, TArg2, TArg3>(this Control control, Action<TArg1, TArg2, TArg3> action, TArg1 arg1, TArg2 arg2, TArg3 arg3) { control.InvokeAction(() => action(arg1, arg2, arg3)); } public static void InvokeAction<TArg1, TArg2, TArg3, TArg4>(this Control control, Action<TArg1, TArg2, TArg3, TArg4> action, TArg1 arg1, TArg2 arg2, TArg3 arg3, TArg4 arg4) { control.InvokeAction(() => action(arg1, arg2, arg3, arg4)); } public static void InvokeAction(this Control control, Delegate d, params object[] args) { control.InvokeAction(() => d.DynamicInvoke(args)); } } }
-
K.Takaoka さん
> 以下は、using WinForms; を行うと、Invoke() の替わりに使える InvokeAction() を提供するという形のイメージ・・・文法チェックのみの手書きなので動作未確認。
「BackgroundWorker は不特定多数のスレッドを消費するような仕組みになっていて」と書かれたのと同じような仕組みということですかね?
メソッドに安全性を隠ぺいしてしまうという発想がとてもスマートだと思いました。
些細なことですが、ここは微妙かなと思ったところが1ヶ所あります。
while (!invokeWorker.Join(0))
(WaitCallback ?? Application.DoEvents)();
DoEvents されることが重要な機能において、WaitCallback を割り当てた場合にはその内容に依存してしまう点です。
以下の方がよくないでしょうかね。
while (!invokeWorker.Join(0))
{
if (WaitCallback != null) WaitCallback();
Application.DoEvents();
}
ただ、先の私のコードで使ってみたのですが、よくわからない現象が2つ発生しました。
1) とても重くなります。CPU の使用率が高くなります。
2) StackOverflowException が発生する場合があります。
1つめの対策として while (!invokeWorker.Join(0)) のループ内に Thread.Sleep(10) を入れたり、Join(10) としたりしてみましたが、同じでした。
今回の検証コードでは非常にたくさんのスレッドが作成されることになりますが、その負荷でしょうか。
2つめは理由として
var invokeWorker = new Thread(() => InvokeAction(control, action))
が再帰になる?(考えられないけど)と思い、素直に control.Invoke(action) にしてみたのですが、変わらず例外が発生しました。いろいろ考えてみたのですが、解りませんでした。 -
> DoEvents されることが重要な機能において、WaitCallback を割り当てた場合には
> その内容に依存してしまう点です。独立したアプリケーションであればよいのですが、ホストアプリケーションを外部に持つ環境で Windows Forms を利用する場合など、アプリケーションの UI スレッドを Application クラスが管理していない場合があります。このような場合、ホスト環境のメッセージ処理を Windows Forms へ反映するため、通常は Application.RegisterMessageLoop() を使用してホストプログラムのメッセージループの状態を登録します。
そうした環境で Application.DoEvents() を呼び出しても多くの場合は問題ないのですが、ホストプログラムが使用しているメッセージループ処理を無視して Windows Message を各ウィンドウへ直接ディスパッチしてしまうため、ホストアプリケーションに致命的な問題をもたらす...という場合もあります。複数の UI スレッドが構築されてしまう典型的な例として、そういった構成のアプリケーションにおいてホスト側とアドオン側が独自の UI スレッドを起動するようなことが想像されるため、Application.DoEvents() を完全に置き換える仕組みは必要になるのでは?と思いました。
経験上必要なので仕掛けておいたとも言えますが、このあたりを考慮されていないホストを破壊してしまって困った経験はあっても、考慮されていて破壊しないように処理が出来て助かった経験はないかもしれない...> 今回の検証コードでは非常にたくさんのスレッドが作成されることになりますが
InvokeAction によって同時に作成されるスレッド数は、イベントの再帰がないかぎり UI スレッドと同じ数までですが、生成と破棄が何度も繰り返されることを考えると再利用可能な形できちんと管理したほうがいいかもしれませんね。他にも、Action 型ではなく Func 型に変更して、Action 型のオーバーロードから Func 型を生成するようにするとか、実用的にするには変更するべき点が多々あるかと思います。アイデアや設計のブループリントとして扱ってください。
あと、間違っていたことですが、BackgroundWorker は UI スレッド上で RunWorkerAsync() を呼び出すと System.Windows.Forms.WindowsFormsSynchronizationContext を利用して UI スレッドで ProgressChanged イベントを発生させるんですね。過去に、ThreadPool に大量の WorkItem を置かれたことがあったと記憶しているのですが、特にそのような問題にはならないようでした。> StackOverflowException が発生する場合があります。
2010/03/18 5:23GMT の投稿に含まれるコードでいうと、UpdateForm1Label() の処理時間より短い頻度で ReportProgress() が呼び出されるためでしょうか。
InvokeAction の中で Application.DoEvents() が実行されるため、Form2 の UI スレッドにてメッセージが処理され続けるため、Form1 の UpdateForm1Label() が完了するより早く次の ProgressChanged イベントが発生します。そうすると、再び InvokeAction() が呼び出されます。
この2回目の InvokeAction の中でも同じことが起こり、3回目の InvokeAction が呼び出されます。一方、Form1 のスレッドが UpdateForm1Label() を完了したとき、既に2回目の InvokeAction のタイムラインが多少進んでいるため、2回目の UpdateForm1Label() の呼び出しは1回目のよりも短い待機時間で発生します。
この誤差が 10% あるとすれば、UpdateForm1Label() が10回呼び出された時点で Form1 の UI スレッドには Form2 からの描画要求が2回分蓄積されている状態になります。この時の Form2 はForm2.ProgressChanged
InvokeAction
Application.DoEvents (11回目の描画完了待ち)
Form2.ProgressChanged
InvokeAction
Application.DoEvents (12回目の描画完了待ち)という状態になっているということです。この状態になると大きな問題が発生します。1つは、11回目の描画が完了しても Application.DoEvents() から処理が帰ってこなくなること、もう1つはおそらく 13 回目、14 回目のの描画がさらに深い階層に追加されてしまうこと(そして、そのうち StackOverflow になること)です。
実際にwhile (!invokeWorker.Join(0))
{
Debug.WriteLine("Enter");
(WaitCallback ?? Application.DoEvents)();
Debug.WriteLine("Leave");
}のようにして実行すると、Enter-Leave が繰り返される中で Enter-Enter-Leave-Leave や、Enter-Enter-Enter-Leave-Leave-Leave というパターンが見えるようになります。1度でも DoEvents に達する前に描画が完了し Leave に到達すれば、親階層の描画とスレッドはすべて完了しているため、すべての Application.DoEvents() と invokeWorker.Join() を抜けて一気に回復します。一時的な Form1 の処理負荷が原因であれば Form2 のスレッドのスタックが尽きる前に回復することもあるでしょう。これは、UI スレッドが複数あることに起因する問題ではなく、単一のスレッドしかない状況でも Application.DoEvents() を呼び出すことによって意図しない再帰が発生するという一般的な危険性です。
この対策として私がよく書く処理フローは、以下のようなかんじです。実際には、Action<int> ではなく BackgroundWorker の DoWorkEventArgs のように UI スレッドからキャンセルするための機能や、UI スレッドに MessageBox.Show を表示させる機能や初期化まわりなどなど、結構なメソッド・プロパティ数を持ったクラスを作成して与えていますので、今適当に書いたものですが、、、private void button1_Click(object sender, EventArgs e) { this.StartThread(DoWork); } private void DoWork(Action<int> ReportProgress) { int count = 0; while (true) { count = (count + 1) % 1000; ReportProgress(count); Thread.Sleep(10); } } public void StartThread(Action<Action<int>> work) { new Thread(RunThread) { IsBackground = true}.Start(work); } private void RunThread(object state) { Action<Action<int>> work = (Action<Action<int>>) state; int progress = 0; var worker = new Thread(() => work(value => progress = value)); worker.Start(); while (!worker.Join(300)) this.Invoke(new Action<int>(this.UpdateLabel), progress); this.Invoke(new Action<int>(this.UpdateLabel), progress); } public void UpdateLabel(int progressValue) { this.label1.Text = progressValue.ToString(); }
-
> このあたりを考慮されていないホストを破壊してしまって困った経験はあっても、考慮されていて破壊しないように処理が出来て助かった経験はないかもしれない...
そういうことですか。
私はそこまでちゃんと考慮できていませんでした。失礼しました。
> InvokeAction によって同時に作成されるスレッド数は、イベントの再帰がないかぎり UI スレッドと同じ数までですが、
今回の場合はイベントハンドラは再入しますので、UI スレッドより増えると思います(ご説明いただいている通りですよね)。
> 1つは、11回目の描画が完了しても Application.DoEvents() から処理が帰ってこなくなること、もう1つはおそらく 13 回目、14 回目のの描画がさらに深い階層に追加されてしまうこと(そして、そのうち StackOverflow になること)です。
なるほど。そういうことですね。
解りやすいご説明、ありがとうございました。