C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[11]: 別スレッドでShowDialogしたフォームのクローズ


(過去ログ 17 を表示中)

[トピック内 12 記事 (1 - 12 表示)]  << 0 >>

■6760 / inTopicNo.1)  別スレッドでShowDialogしたフォームのクローズ
  
□投稿者/ 困ったちゃん (1回)-(2007/08/23(Thu) 12:57:42)

分類:[VB.NET/VB2005] 

お尋ねします。
メインフォームForm1から別スレッドで経過表示用フォームForm2をShowDialog表示する下記のコードで、
書き込み処理中にForm2のクローズボックスをクリックすると、Invokeのところで凍りついてしまいます。
どこに問題があるのでしょうか。
開発環境・使用言語はVB2003/Framework1.1です。

'/*** Form1 (メインフォーム) ***/
Public Class Form1 : Inherits System.Windows.Forms.Form
    '// Friend WithEvents Button1 As System.Windows.Forms.Button
    '// Friend WithEvents Button2 As System.Windows.Forms.Button
    Private fm2 As Form2

    '// Button1.Click : Form2を別スレッドで開く
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        If fm2 Is Nothing Then
            Dim t As New System.Threading.Thread(AddressOf Me.ShowForm2)
            t.Start()
        End If
    End Sub

    '// Form2を開くためのプロシージャ
    Private Sub ShowForm2()
        fm2 = New Form2
        fm2.ShowDialog()
        fm2.Dispose()
        fm2 = Nothing
    End Sub

    '// Button2.Click : 経過記録を表示しながら行う繰り返し処理
    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        Me.Button2.Enabled = False
        For i As Integer = 1 To 1000
            Me.WriteLine(i.ToString() & "巡目です。")
        Next
        Me.Button2.Enabled = True
    End Sub

    '// Form2に1行表示
    Private Sub WriteLine(ByVal text As String)
        If fm2 Is Nothing Then Return
        Try
            fm2.WriteLine(text)
        Catch ex As System.ObjectDisposedException
        End Try
    End Sub
End Class

'/*** Form2 (経過表示用フォーム) ***/
Public Class Form2 : Inherits System.Windows.Forms.Form
    '// Friend WithEvents RichTextBox1 As System.Windows.Forms.RichTextBox 
    Private Delegate Sub WriteLineDelegate(ByVal text As String)
    Private dlgt As New WriteLineDelegate(AddressOf Me.WriteLine)

    '// リッチテキストボックスに文字を追記表示する
    Public Sub WriteLine(ByVal text As String)
        If Me.IsDisposed OrElse Not Me.IsHandleCreated Then Return
        If Me.InvokeRequired Then
            Me.Invoke(dlgt, New Object() {text})    '// ここで凍りつく
        Else
            With Me.RichTextBox1
                .AppendText(text & vbCrLf)
                .ScrollToCaret()
                .Focus()
            End With
        End If
    End Sub
End Class


なお、Form1.ShowForm2の fm2.Dispose() : fm2 = Nothing を外しても状況は変わりませんでした。

引用返信 編集キー/
■6772 / inTopicNo.2)  Re[1]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ まどか (363回)-(2007/08/23(Thu) 16:39:03)
fm2は別スレッドで作成したのだから

> '// Form2に1行表示
> Private Sub WriteLine(ByVal text As String)
> If fm2 Is Nothing Then Return
> Try
> fm2.WriteLine(text)
> Catch ex As System.ObjectDisposedException
> End Try
> End Sub

これこそInvokeする必要があるのでは。

逆に

> '// リッチテキストボックスに文字を追記表示する
> Public Sub WriteLine(ByVal text As String)
> If Me.IsDisposed OrElse Not Me.IsHandleCreated Then Return
> If Me.InvokeRequired Then
> Me.Invoke(dlgt, New Object() {text}) '// ここで凍りつく
> Else
> With Me.RichTextBox1
> .AppendText(text & vbCrLf)
> .ScrollToCaret()
> .Focus()
> End With
> End If
> End Sub

こっちは自分自身の操作だからInvokeは要らないのでは。
引用返信 編集キー/
■6774 / inTopicNo.3)  Re[2]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ 困ったちゃん (3回)-(2007/08/23(Thu) 17:43:28)
No6772 (まどか さん) に返信

アドバイスありがとうございます。

Form1側のWriteLineを

'/*** Form1 (メインフォーム) ***/

    Private Delegate Sub WriteLineDelegate(ByVal text As String)
    Private dlgt As New WriteLineDelegate(AddressOf Me.WriteLine)

    '// Form2に1行表示
    Private Sub WriteLine(ByVal text As String)
        If fm2 Is Nothing Then Return
        If fm2.IsDisposed OrElse Not fm2.IsHandleCreated Then Return
        Try
            If fm2.InvokeRequired Then
                fm2.Invoke(dlgt, New Object() {text})   '// ここで凍りつく
            Else
                fm2.WriteLine(text)
            End If
        Catch
        End Try
    End Sub

と改め、かつForm2側のWriteLineを

'/*** Form2 (経過表示用フォーム) ***/

    '// リッチテキストボックスに文字を追記表示する
    Public Sub WriteLine(ByVal text As String)
        With Me.RichTextBox1
            .AppendText(text & vbCrLf)
            .ScrollToCaret()
            .Focus()
        End With
    End Sub

としたコードでも、If分岐が移っただけなので、やはり fm2.Invoke のところで凍りついてしまいます。
ご指摘いただいた意図と食い違っていますでしょうか?

引用返信 編集キー/
■6779 / inTopicNo.4)  Re[3]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ 困ったちゃん (5回)-(2007/08/23(Thu) 18:31:56)
すみません。Form1.WriteLine では fm2.InvokeRequired を検査する意味がありませんでした。単純に;

'/*** Form1 (メインフォーム) ***/

    Private Delegate Sub WriteLineDelegate(ByVal text As String)

    '// Form2に1行表示
    Private Sub WriteLine(ByVal text As String)
        If fm2 Is Nothing Then Return
        If fm2.IsDisposed OrElse Not fm2.IsHandleCreated Then Return
        Try
            Dim dlgt As New WriteLineDelegate(AddressOf fm2.WriteLine)
            fm2.Invoke(dlgt, New Object() {text})
        Catch
        End Try
    End Sub

としてみましたが、やはり結果は同じでした。

引用返信 編集キー/
■6791 / inTopicNo.5)  Re[4]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ まどか (364回)-(2007/08/23(Thu) 21:03:36)
まぁあてずっぽで言うと、モーダルが原因かも。
モードレスでインスタンスが生きている間Sleepループかけたらうまくいくかもしれません。
#予想ですいません

というか本来言いたいことはそうではなくて
普通は時間のかかる「処理」を別スレッド化して、GUIメインスレッドは通常の入力待ち状態(普通にフォームが表示されている状態)にさせます。
で、スレッドからの通知を受けて表示を更新します。

下記にて、まずはやろうとしていることとの違いというか一般的な形を試してみるとよいと思います。

http://dobon.net/vb/
Tips − その他のTips − 「時間のかかる〜」サンプル
引用返信 編集キー/
■6793 / inTopicNo.6)  Re[5]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ れい (52回)-(2007/08/23(Thu) 21:20:57)
2007/08/23(Thu) 21:23:57 編集(投稿者)
2007/08/23(Thu) 21:22:42 編集(投稿者)

No6772 (まどか さん) に返信
> fm2は別スレッドで作成したのだから
>> '// Form2に1行表示
>> Private Sub WriteLine(ByVal text As String)
> これこそInvokeする必要があるのでは。
>> '// リッチテキストボックスに文字を追記表示する
>> Public Sub WriteLine(ByVal text As String)
> こっちは自分自身の操作だからInvokeは要らないのでは。

そんなことはないですよ。
Formのメンバを呼ぶときに作成元スレッドであればいいので、
WriteLineでチェックするやり方もアリです。
そのほうが綺麗になる場合が多い。

No6760 (困ったちゃん さん) に返信
> 書き込み処理中にForm2のクローズボックスをクリックすると、Invokeのところで凍りついてしまいます。
> どこに問題があるのでしょうか。

フォームが閉じて作成元スレッドはメッセージを読みにいかなくなるのに
他のスレッドがInvokeでメッセージを送り、完了を待つからです。

No6791 (まどか さん) に返信
> というか本来言いたいことはそうではなくて
> 普通は時間のかかる「処理」を別スレッド化して、GUIメインスレッドは通常の入力待ち状態(普通にフォームが表示されている状態)にさせます。
> で、スレッドからの通知を受けて表示を更新します。
> 下記にて、まずはやろうとしていることとの違いというか一般的な形を試してみるとよいと思います。

まどかさんのおっしゃるように、
GUIはGUIスレッドに、処理は処理用のスレッドに、というのが普通です。
ですが、コンポーネント化などの都合で、違う方法をとることもあります。
今回のようなパターンも、有効な場合もあります。

Invokeではなく、
BeginInvokeにすれば完了を待たないので解決するかと思います。
また、その方がパフォーマンスも優れているはずです。
WriteLineしてる間処理ができないのはもったいないですから。

注意点は、
BeginInvokeだと完了を待たないので戻り値を取得できませんし、
Invoke先がきちんと完了したかわかりません。
また、前回の完了もわからないので、
たくさん呼ぶとキューにたくさん溜まります。
メッセージキューを使ってるので、あまりたくさん溜められません。
引用返信 編集キー/
■6798 / inTopicNo.7)  Re[6]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ 困ったちゃん (6回)-(2007/08/23(Thu) 23:29:22)
No6791 (まどか さん) に返信
> 普通は時間のかかる「処理」を別スレッド化して、GUIメインスレッドは通常の入力待ち状態(普通にフォームが表示されている状態)にさせます。
> で、スレッドからの通知を受けて表示を更新します。

おっしゃることはよく分かります。No6760 のコードは、できるだけ簡潔に記述しようとしたため、Form1に重たい処理が載ってしまいましたが、
もとよりGUIメインスレッドの方を重くすることが主旨ではありません。ワーカースレッドに振っても構いません。
そもそもの動機は、多回数の繰り返し処理の途中情報を表示するフォームを、好きなときに開いたり閉じたりしたいという点でした。
その代替手段はいろいろ考えられるのでしょうが、それはまた別問題として、このサンプルコードでフリーズしてしまう理由がどうにも釈然と
しないので、お尋ねしたような次第です。

#ここで各方面からツッコミが入る前に、一点、イクスキューズを。
好きなときにButton1を押せるようにするには、Form1.Button2_Click のループ中にApplication.DoEvents()を挿入すべきですが、
そうすると、Form2のクローズボタンを押してもフリーズしにくくなります(ただし数回〜数十回程度に1回はフリーズします)。
サンプルコードでは、再現性を損なわず問題点が際立つように、DoEventsは省きました。

No6793 (れい さん) に返信
> フォームが閉じて作成元スレッドはメッセージを読みにいかなくなるのに
> 他のスレッドがInvokeでメッセージを送り、完了を待つからです。

まさにその辺りの事情を詳しく知りたいのです。解説ドキュメントなどご存知でしたらご教示いただけませんでしょうか。

> Invokeではなく、
> BeginInvokeにすれば完了を待たないので解決するかと思います。

InvokeをBeginInvokeに書き換えると、速くなりすぎてForm2のクローズボタンを掴まえられなくなりました。^^;
何か工夫して確認にトライしてみます。
ただ、このサンプルコードでは戻り値を必要としませんでしたが、一般にはFunctionをInvokeしたいケースも多々あろうかと存じます。
この手の問題の正統?な解決策がありましたら、併せてお願いいたします。

Form2のClosingイベントでフラグを立てて、Invokeを阻止すると効果があることは確認できたのですが、それで万全なのかが確信できないでいます。
引用返信 編集キー/
■6802 / inTopicNo.8)  Re[7]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ れい (53回)-(2007/08/24(Fri) 00:46:24)
No6798 (困ったちゃん さん) に返信
> ■No6793 (れい さん) に返信
>>フォームが閉じて作成元スレッドはメッセージを読みにいかなくなるのに
>>他のスレッドがInvokeでメッセージを送り、完了を待つからです。
>
> まさにその辺りの事情を詳しく知りたいのです。解説ドキュメントなどご存知でしたらご教示いただけませんでしょうか。

Undocumentedだと思います。
普通これ以上詳しく知る必要はないと思いますが、
WndProcをオーバーライドして送られてくるメッセージを見るとか、
エラーを起こしたりデバッグ環境からスタックトレースを見るとか、
ILを眺めるとかすればわかります。

> InvokeをBeginInvokeに書き換えると、速くなりすぎてForm2のクローズボタンを掴まえられなくなりました。^^;
> 何か工夫して確認にトライしてみます。

Sleepでもいれてください。
進捗状況を表示するなら当然重い作業なわけで、
適当な間隔で呼べば動作します。

> ただ、このサンプルコードでは戻り値を必要としませんでしたが、一般にはFunctionをInvokeしたいケースも多々あろうかと存じます。
> この手の問題の正統?な解決策がありましたら、併せてお願いいたします。
> Form2のClosingイベントでフラグを立てて、Invokeを阻止すると効果があることは確認できたのですが、それで万全なのかが確信できないでいます。

Closingにフラグ組み込むだけではだめです。
Form.CloseされてからClosingに入る前にInvokeされたら止まります。

このControl.Invoke系のデザインは、非常に問題があると思っています。
(と言っても他のいいデザインは思いつきませんが。)
Invoke系を100%安全に使おうとするなら、

・ログとかで呼び出しが重要でない場合はBeginInvoke投げっぱなし
・戻り値が欲しいならBeginInvoke+IAsyncResultでタイムアウトを設定

この二つしかありません。
メッセージの先読みとかそういった壮大な仕掛けを用意すればほぼ100%確実になりますが、
それでもプログラミング論的には間違いです。

マルチスレッドやスレッド間通信に慣れれば、
このControl.Invokeの問題がわかると思います。
そもそも、他スレッドに対して強制的にメッセージを伝え、
伝わるまで待つデザインはあり得ません。

スレッドは、死んでることはわかりますが、
生きていることを保証することはできません。
固有のUIを持つスレッドなどは特に、いつ死ぬかわかりません。
こういったスレッドへ情報を伝達し、
その伝達自体を保証する方法はありません。
「伝達できたかできなかったか知る」ことはできます。

Invokeも全く同じ問題です。
ひどいのは、Control.Invokeが、メッセージが伝達されるまで待つ点で、
つまり、Control.Invokeを安全に使う一般的方法はありません。
(実質安全である場合はありますが)
引用返信 編集キー/
■6805 / inTopicNo.9)  Re[8]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ れい (55回)-(2007/08/24(Fri) 01:19:35)
2007/08/24(Fri) 04:08:59 編集(投稿者)

No6802 (れい さん) に返信・抜粋
> このControl.Invoke系のデザインは、非常に問題があると思っています。
> つまり、Control.Invokeを安全に使う一般的方法はありません。

なので私はControl.Invokeは使いません。
バグの温床になるので、ライブラリから削除すべきメソッドだと思っています。
(BeginInvokeのつもりで間違って書いちゃうので。)

あちこちのサイトにControl.Invokeのサンプルがあります。
試したわけじゃないですが、ソースを見る限り何も対策してないので、
フォームを閉じると今回のような問題が発生すると思われます。
Control.Invokeをきちんと使える方法はどこにも見つけられませんでした。

でも本当は、使えたらうれしいな、と思ってるので、

> つまり、Control.Invokeを安全に使う一般的方法はありません。

これに対する意見、特に反論が欲しいです。
スレ立てでもしてみようかしら。
教えて!偉い人。
引用返信 編集キー/
■6822 / inTopicNo.10)  Re[9]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ 困ったちゃん (7回)-(2007/08/24(Fri) 12:18:47)
No6802 (れい さん) に返信

詳細なアドバイス、ありがとうございます。

> Undocumentedだと思います。

そうですか。残念です。

> Closingにフラグ組み込むだけではだめです。
> Form.CloseされてからClosingに入る前にInvokeされたら止まります。

そうなんです。原コード■No6760 のForm2を;

'/*** Form2 (経過表示用フォーム) ***/
Public Class Form2 : Inherits System.Windows.Forms.Form
    '// Friend WithEvents RichTextBox1 As System.Windows.Forms.RichTextBox 
    Private Delegate Sub WriteLineDelegate(ByVal text As String)
    Private dlgt As New WriteLineDelegate(AddressOf Me.WriteLine)
    Private blockedInvoking As Boolean                  '//  Closingイベントフラグ

    '// リッチテキストボックスに文字を追記表示する
    Public Sub WriteLine(ByVal text As String)
        If Me.IsDisposed OrElse Not Me.IsHandleCreated Then Return
        If Me.InvokeRequired Then
            If blockedInvoking Then Return '             // Closing中ならばInvokeを阻止
            Try
                Me.Invoke(dlgt, New Object() {text})
            Catch ex As System.ObjectDisposedException  '// 破棄後のアクセスは無視
            End Try
        Else
            With Me.RichTextBox1
                .AppendText(text & vbCrLf)
                .ScrollToCaret()
                .Focus()
            End With
        End If
    End Sub

    '// フォームを閉じようとしたときにフラグを立てる
    Private Sub Form2_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
        blockedInvoking = True
        '                                                // 以下は『確認のためのコード』
        Me.WriteLine("blockedInvoking = True")
        Static Dim retry As Boolean
        If Not retry Then e.Cancel = True '              // 1回目のClosingのみキャンセル
        retry = True
    End Sub
End Class

と書き換えて、書き込み中にクローズボタンを押して表示を止めてみると、

        1巡目です。
        ...
        163巡目です。
        164巡目です。
        blockedInvoking = True
        165巡目です。
        (停止)

のようになって、フラグを立てた後でも1回Invokeに進んでしまうことが判ります。
で、改めて『確認のためのコード』以下を外すと、最後のInvokeがObjectDisposedException
をスローするので、Catch句で殺しています。(原コードではForm1側に記述していました)

百回程度試行してみましたが、上記のコードだとフリーズすることはありませんでした。

しかし原コードでも、ObjectDisposedExceptionがキャッチされずにフリーズしていたので、
果たして『それで万全なのかが確信できない』というわけです。
必ず例外を投げてくれるのなら(エレガントとは言えませんけど^^;)安心なのですが。

引用返信 編集キー/
■6840 / inTopicNo.11)  Re[10]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ れい (57回)-(2007/08/24(Fri) 17:50:56)
No6822 (困ったちゃん さん) に返信
> 百回程度試行してみましたが、上記のコードだとフリーズすることはありませんでした。
> しかし原コードでも、ObjectDisposedExceptionがキャッチされずにフリーズしていたので、
> 果たして『それで万全なのかが確信できない』というわけです。
> 必ず例外を投げてくれるのなら(エレガントとは言えませんけど^^;)安心なのですが。

投げる側のスレッドがフラグをチェックしてInvokeメソッドに入って内部で実際にメッセージを投げるまでの間に、
受ける側のスレッドがClosingに入ってそのままWM_DESTROY(Close)まで全部処理できることは
たぶん稀ですので、100回程度ではだめでしょう。
この手の問題はメモリが足りないときとか、遅いPCを使ったりとかした場合にのみたまにエラーが出たりするので
かなり厄介です。

そもそもClosingイベントが呼ばれないであるメッセージで突然フォームが死ぬ場合もありえるので
(WM_DESTROYやWM_QUITを投げられた場合など。)
やっぱりInvokeはダメダメだと思います。

引用返信 編集キー/
■6841 / inTopicNo.12)  Re[11]: 別スレッドでShowDialogしたフォームのクローズ
□投稿者/ 困ったちゃん (8回)-(2007/08/24(Fri) 18:27:45)
2007/08/24(Fri) 18:30:20 編集(投稿者)

No6840 (れい さん) に返信
> やっぱりInvokeはダメダメだと思います。

そうなると、こういった用例は数多いだけに、ホントに困ったちゃんですね。

全面解決とはなりませんでしたが、おかげ様で問題の本質は浮かび上がりましたので、
このトピックはひとまず『解決済み』とさせていただきます。
実務実装の方は、現実的なリスクを勘案しながら、アレコレ模索することにいたします。

ありがとうございました。

No6805 (れい さん) を引用
>>つまり、Control.Invokeを安全に使う一般的方法はありません。
> これに対する意見、特に反論が欲しいです。
> スレ立てでもしてみようかしら。
> 教えて!偉い人。

もし他の掲示板で立てられるのでしたら、
その折はここにも置き書きしていただけると幸甚です。
解決済み
引用返信 編集キー/


トピック内ページ移動 / << 0 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -