- PR -

FormのClosingイベントの実行スレッドについて

1
投稿者投稿内容
peeza
会議室デビュー日: 2005/06/11
投稿数: 4
投稿日時: 2005-06-11 23:55
BCBからの乗換えをもくろんでいる.net初心者です。

フォームを左上の×ボタンをクリックしてクローズした時に、そのフォームで作成したスレッドの停止をさせるコードを書いています。

まず、明示的にボタンを押してスレッドを停止させるstopButtonのイベントハンドラを以下のように用意しました。backupThreadがフォームで作成して実行されているスレッドオブジェクトです。

private void stopButton_Click(object sender, EventArgs e)
{
stopEvent.Set();
backupThread.Join();
backupThread = null;
panel1.Text = "Ready...";
}

次に、スレッド実行中に×ボタンをクリックしてクローズした時用の
Form.ClosingのイベントハンドラをForm1のコンストラクタで追加しました。

//Form1() の中のコード
Closing += new CancelEventHandler(Form1_Closing);

//Closingのイベントハンドラ本体
void Form1_Closing(object sender, CancelEventArgs e)
{
if (backupThread != null)
{
stopButton.Invoke(new System.EventHandler(this.stopButton_Click), new     object[] { this, new EventArgs() });
}
}

ここで、先ほど作っておいたstopButtonのイベントハンドラを呼び出しているのですが、stopButton_Click(this, new EventArgs())と呼び出すとプログラムがとまってしまうので、上記のようにデリゲートを通してstopButtonの生成されたスレッドに同期させてstopButton_Clickを呼び出しすとうまくいきます。

Q1これは、Form.Closingがそれに含まれるコントロールと別のスレッドで動作するからのでしょうか?そして、この呼び出し方法は正しいのでしょうか?

あるButtonのイベントハンドラを用意して、別のButtonのイベントハンドらから前者のイベントハンドラを直接コールしても問題なく動作します。

private void button1_Click(object sender, EventArgs e)
{
panel1.Text = "test";
}

private void button2_Click(object sender, EventArgs e)
{
button1_Click(this, new EventArgs());
}

いまいち、アプリケーションの実行スレッド、各イベントの実行スレッド、非同期呼び出し時の呼び出されたコードの実行スレッドの概念があいまいでして。

Q2Form.ClosingはFormデザイナのPropertyに表示されていないようですが、コードで追加するのが正しいのでしょうか?

よろしくお願いします。

[ メッセージ編集済み 編集者: peeza 編集日時 2005-06-11 23:58 ]
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2005-06-12 00:52
引用:

Q1これは、Form.Closingがそれに含まれるコントロールと別のスレッドで動作するからのでしょうか?



違います。
Form.Closing は、Form を生成したスレッドのコンテキストで呼び出されます。

引用:

そして、この呼び出し方法は正しいのでしょうか?



「この呼び出し方法」とは、stopButton_Click() を Form.Invoke() していることですか?

理論は正しくありませんが、結果として期待する動作を得られています。
このままでも大間違いではありません。

Form.Closing で stopButton_Click() を普通に呼び出すとブロックしてしまうのは、プログラムの構造上の問題です。
おそらく backupThread 内部でログ表示など UI を操作している箇所があるはずで、それらをコメントアウトして試してみてください。

stopButton_Click() 内の Join() によって、Form 以下のコントロールを生成したスレッドは backupThread が終了するまでブロックします。
その状態で backupThread から UI 操作を行うと、メッセージポンプがデッドロックしてしまうのです。

引用:

Q2Form.ClosingはFormデザイナのPropertyに表示されていないようですが、



イベントとプロパティは別物です。
イベントの設定は、プロパティ編集画面の情報のツールバー内の稲妻のアイコンのボタンを押下すと表示される画面で行います。


_________________
// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/
peeza
会議室デビュー日: 2005/06/11
投稿数: 4
投稿日時: 2005-06-12 10:42
ありがとうございます!
おっしゃる通り、backupThread内でStatusBarの表示(Invokeで作成スレッドに同期させています)を行っていました。コメントアウトすることでInvokeせずに実行できました。

Qしかし、backupThread内でUI操作をしつつ、そのスレッドの停止待ちをする正しい方法とはどういうふうに行うべきなのでしょうか?

backupThreadの処理の最後にイベントを入れて、backupThread.Join()の代わりにイベント待ち(WaitOne())をしてみたのですが、やはりデッドロックを起こしてしまうみたいです。WaitOne()もスレッドをポーリングを行って、スレッドをロックしているだけということでしょうか…。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2005-06-12 14:25
引用:

Qしかし、backupThread内でUI操作をしつつ、そのスレッドの停止待ちをする正しい方法とはどういうふうに行うべきなのでしょうか?



簡単で効果的な方法はなかなか思いつかないですね。

簡単なツール程度なら、Thread.IsBackgorund = true にしておいて、Closing で

・Form.Hide()
・適当な時間 Thread.Sleep();

してしまうこともあります>わたし

引用:

backupThreadの処理の最後にイベントを入れて、backupThread.Join()の代わりにイベント待ち(WaitOne())をしてみたのですが、やはりデッドロックを起こしてしまうみたいです。
WaitOne()もスレッドをポーリングを行って、スレッドをロックしているだけということでしょうか…。



WaitOne() が内部でポーリングを行っているわけではありませんが、待ち合わせの対象がスレッド終了からスレッド終了時にシグナル状態になるイベントに置き換わっただけで、本質的な問題(=プライマリスレッドのメッセージポンプを止めてしまう)に変化はないので、まったく同じ現象になるのは当然です。

前述したように、スレッドからの UI 操作が必要なら、メッセー十ポンプを回しつつスレッドの終了を待ち合わせるような「構造上の改良」が必要です。

メッセージポンプを回し続けるためには、Form.Closing で一旦 Form のクローズをキャンセルしなければなりません。

その上で、タイマーでスレッドの終了をポーリングしたり、スレッド終了間際に Form.Invoke() で Form にワーカスレッドの終了を通知するなどして、全ワーカースレッドの終了を確認してから、Form をクローズするような感じになると思います。

_________________
// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/
peeza
会議室デビュー日: 2005/06/11
投稿数: 4
投稿日時: 2005-06-14 01:20
渋木さん、非常に的確なご返答ありがとうございます!

私も土日にMSDNのアーティクルとかを見ながら勉強しようとはしていたのですが、身にならず平日に突入してしまいました。
どうも、メソッドの実行の概念がいまいクリアーにならない状況です。

例えば、今回のケースでも、BCB+Win32では、Closing(BCBではの対応イベントでは)の中でWM_CLOSEをWin32 APIを使ってPOSTしておけば、Closingをキャンセル終了した後に再度、フォームクローズのイベントをスケジューリングできたのですが…。

Q.netでメッセージループが見えない場合(見ないようにプログラムする場合)、あるイベントの中からそのイベントまたは他のイベントを、そのイベント終了後にスケジューリングすることは可能なのでしょうか?ことに、Closeのイベントなどは、ユーザーが右上の×を押した操作をコードから発動させることは可能なのでしょうか?(Win32では特定のメッセージをwindowに送ればよかったと思うのですが)

>簡単なツール程度なら、Thread.IsBackgorund = true にしておいて、Closing で
>・Form.Hide()
>・適当な時間 Thread.Sleep();

Qこの方法のThread.Sleep();を行っているところでは、backupThreadからInvokeされたUI操作に一時的に、スレッドを明渡すような感じになるのでしょうか?

すみません、もう一つだけお願いします。
QInvokeを呼び出すオブジェクトの作成スレッドが現在の実行スレッドである場合、Invokeして呼び出したデリゲートに関連付けられているメソッドを、直接コールするのとは実行のタイミングなどで違いがでてくるのでしょうか?

質問ばかりで申し訳ありませんが、自力で解説されている記事や書籍をみつけることができませんでした。参考文献や参考サイトなどでもよろしいので、教えていただけませんでしょうか?
なちゃ
ぬし
会議室デビュー日: 2003/06/11
投稿数: 872
投稿日時: 2005-06-14 11:19
引用:

peezaさんの書き込み (2005-06-14 01:20) より:
Q.netでメッセージループが見えない場合(見ないようにプログラムする場合)、あるイベントの中からそのイベントまたは他のイベントを、そのイベント終了後にスケジューリングすることは可能なのでしょうか?


BeginInvokeが一応それにあたりますが、EndInvokeを呼ばなければならないことになっているので、Fire and Forget では使いにくかったりします。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2005-06-14 19:40
引用:

BCB+Win32では、Closing(BCBではの対応イベントでは)の中でWM_CLOSEをWin32 APIを使ってPOSTしておけば、Closingをキャンセル終了した後に再度、フォームクローズのイベントをスケジューリングできたのですが…。



PostMessage() API を P/Invoke すれば、「まったく同じこと」が .NET でも出来ます。

引用:

Q.netでメッセージループが見えない場合(見ないようにプログラムする場合)、あるイベントの中からそのイベントまたは他のイベントを、そのイベント終了後にスケジューリングすることは可能なのでしょうか?ことに、Closeのイベントなどは、ユーザーが右上の×を押した操作をコードから発動させることは可能なのでしょうか?



なちゃさんのコメントの通りです。
Invoke() が SendMessage(), BeginInvoke() ~ EndInvoke() が PostMessage() に相当します。

引用:

Qこの方法のThread.Sleep();を行っているところでは、backupThreadからInvokeされたUI操作に
一時的に、スレッドを明渡すような感じになるのでしょうか?



backupThread そのものに対して、スムースに実行が切り替わることを期待してます。

以前にも書きましたが、「Invoke したら」そのコントロール(ウィンドウ)を生成したスレッドに制御が切り替わり、そのスレッドのコンテキストでコードが実行されます。

引用:

QInvokeを呼び出すオブジェクトの作成スレッドが現在の実行スレッドである場合、Invokeして呼び出したデリゲートに関連付けられているメソッドを、直接コールするのとは実行のタイミングなどで違いがでてくるのでしょうか?



Invoke は SendMessage() 相当なので、基本的には即時実行されます。

_________________
// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/
peeza
会議室デビュー日: 2005/06/11
投稿数: 4
投稿日時: 2005-06-20 00:54
なちゃさん、渋木さん、親切なご返答ありがとうございました。
ご報告が遅くなってすみません。ある程度まとまった内容をご報告しようと思いまして時間がかかってしまいました。

頂いた返信の内容やMSDNで見つけた記事(http://msdn.microsoft.com/msdnmag/issues/04/01/NET/)を参考に、結局以下のように実装しました。(抜粋です)
コード:
//Form1コンストラクタの中でCloseを呼び出すデリゲートを作成しました
//delegate void CloseDeligate();
//CloseDeligate closeDeligate;
//closeDeligate = new CloseDeligate(Close);

void ExcuteBackup()
{
  TimeSpan restTimeSpan = new TimeSpan();
  while (stopEvent.WaitOne(1000, false) == false)
  {
    restTimeSpan = backupDateTime - DateTime.Now;
    UpdateStatusBar(restTimeSpan.ToString());
  }
  EnableStart(true);
}

void UpdateStatusBar(string text)
{
  if (InvokeRequired == true)
  {
    BeginInvoke(updateStatusBarDeligate, new object[] { text });
    return;
  }
  panel1.Text = text;
}

void EnableStart(bool enabled)
{
  if (InvokeRequired == true)
  {
    BeginInvoke(enableStartDeligate, new object[] { enabled });
    return;
  }
  startButton.Enabled = enabled;
  stopButton.Enabled = !enabled;
}

private void stopButton_Click(object sender, EventArgs e)
{
  stopEvent.Set();
}

private void OnClosing(object sender, CancelEventArgs e)
{
  if (backupThread != null)
  {
    if (backupThread.IsAlive == true)
    {
       stopEvent.Set();
       Thread.Sleep(1000);
       BeginInvoke(closeDeligate);
       e.Cancel = true;
       return;
    }
  }
}



backupThreadの実行メソッドでやりたかったことの概要は、
backupDateTimeにバックアップする日時が格納されているとして、
1秒ごとにstatusBar1の内容を書き換え、バックアップする日時がきた時点で
バックアップ処理を実行して終了するというものです。
他にTimerを使うなどの適切なやり方があるようですが、いまはThreadのトレーニングとして
この方法で実装しようとしています。

メインスレッドのUI操作メソッドの中で、別スレッドから呼び出されたときは、BeginInvokeを
用いて自分自身を再スケジュールするようにしました。どうも、Fire and forgetの呼び出しでは、
EndInvokeはいらないようです。(<-MSDNのサンプルより)

Closingの中身は、スレッドが生きていたらstopEventをシグナルして、
ちょっと待って(backupThreadからメインスレッドへのBeginInvokeをスケジューリング
する時間)Closeメソッドを再スケジュールしてリターンするようにしました。
これで、backupThreadが生きている間にFormをCloseしてもbackupThreadの停止を
確認してからメインスレッドを落とせそうです。(step実行の結果)

1

スキルアップ/キャリアアップ(JOB@IT)