BackgroundWorkerでRunWorkerCompletedが発生しない

解決


かまんべーる  2011-10-27 03:17:41  No: 147380

マルチスレッドの処理を実行したいと思っています。

サンプルで以下のようなコードを作ったのですが、DoWork終了後にRunWorkerCompletedが発生しません。
コントロールはフォームにテキストボックスとボタンを配置しています。
------ここから------

Public Class Form1

    Private PI_Cnt As Short
    Private PB_Cancel As Boolean
    Private PB_End As Boolean

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim DI_Clc As Short

        '処理が行われているときは、何もしない
        If BackgroundWorker1.IsBusy Then
            Return
        End If

        PB_End = False
        PI_Cnt = 0
        PB_Cancel = False

        BackgroundWorker1.RunWorkerAsync(TextBox1.Text)

        DI_Clc = fnMain()

        Do
            If BackgroundWorker1.IsBusy = False Then
                Exit Do
            End If
        Loop

        MessageBox.Show(DI_Clc + PI_Cnt)

    End Sub

    Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

        ' BackgroundWorkerの取得(スレッドを作成したオブジェクト)
        Dim objWorker As System.ComponentModel.BackgroundWorker = CType(sender, System.ComponentModel.BackgroundWorker)

        Dim DI_Cnt As Short

        Dim DS_String As String

        DS_String = e.Argument

        For DI_Cnt01 As Short = 1 To Val(DS_String)
            System.Threading.Thread.Sleep(50)
            DI_Cnt += 1
        Next
        e.Result = DI_Cnt

    End Sub

     Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted

        ' 最初に、例外がスローされた場合の処理
        If Not (e.Error Is Nothing) Then
            MessageBox.Show(e.Error.Message)
        ElseIf e.Cancelled Then
            ' 次に、ユーザーが計算をキャンセルした場合の処理
            PB_Cancel = True
        Else
            ' 正常に完了した場合の処理
            PB_Cancel = False
        End If
        PI_Cnt = e.Result

     End Sub

    Private Function fnMain() As Short

        Dim DI_Cnt As Short
        Dim DI_Calc As Short

        For DI_Cnt = 1 To 50
            System.Threading.Thread.Sleep(50)
            DI_Calc += 1
        Next

        Return DI_Calc

    End Function

End Class

------ここまで------

「If BackgroundWorker1.IsBusy = False Then」の部分でFalseが帰ってこないので永久にループし続けます。

なぜRunWorkerCompletedが発生しないのでしょうか?

開発はVisualStudio2008です。


魔界の仮面弁士  2011-10-27 05:14:27  No: 147381

>  For DI_Cnt = 1 To 50
>      System.Threading.Thread.Sleep(50)
>      DI_Calc += 1
>  Next
UI スレッド(ウィンドウを作成するスレッド)から Sleep メソッド(あるいは Sleep API)を呼ぶことは避けてください。
ウィンドウを持たないスレッドから呼ぶのであれば問題ありませんが。

> 「If BackgroundWorker1.IsBusy = False Then」の部分でFalseが帰ってこないので永久にループし続けます。
DoEvents を呼び出すなどすれば、IsBusy プロパティが変化するようにはなります。

……が、そもそもこういった「ループ監視」を行うことは避けてください。
これではメインスレッドがビジー状態になってしまいますので、IsBusy どころか
RunWorkerCompleted の通知さえ受け取れなくなります。

ワーカースレッドの完了は、RunWorkerCompleted イベントで「通知」されてきますので、
わざわざ IsBusy の状態を監視し続ける必要はありません。
RunWorkerAsync 呼び出し後は、そのまま即座に Return 等で抜けてしまい、
完了処理は RunWorkerCompleted イベントにて行うようにしましょう。


かまんべーる  2011-10-27 19:31:50  No: 147382

ご回答ありがとうございます。

スリープの件は、ウェイトを置くために今回付けたもので実際のプログラムでは存在しません。

IsBusyの件はこれをしている理由があります。
一つのファイルを開いたとき、そのファイルを表示するフォームの用意に時間がかかります。
またこのファイルに付随する情報のプール作業があります。
フォームに表示する際にプールした情報が必要になるのですが、ストレートに流すとフォームの用意、プール、表示となり時間がかかるので、フォームの用意とプールを並列で作業して時間の短縮を図っています。
問題は、用意とプールでどちらが先に終了するかわからないが、表示する段階ではプールが終わっていないといけない。
なので監視をする必要があります。

これに上手く対応する方法はありますでしょうか?


YuO  2011-10-27 20:01:27  No: 147383

フォームの用意に時間がかかる,というのは意味がわからないのですが……。
フォームに表示するためのデータの準備,というのであれば,それも元々バックグラウンドで行うべきです。
# 描画に時間がかかる,という内容では無さそうですし。

とりあえず,
・Button1_Clickではフォームの用意を行う
→BackgroundWorker1の処理が終了するかどうかは気にしない
・BackgroundWorker1_RunWorkerCompletedで用意したフォームの表示を行う
とすれば,解決するはずです。
Button1_Clickから制御を戻さない限り,RunWorkerCompletedイベントは発生しないのですから。


かまんべーる  2011-10-27 23:23:47  No: 147384

コントロールの数が多い、フォームの大きさに合わせて調整されるコントロールなどがあるという点からフォームの用意(フォームの大きさに合わせるという点からLOADとは異なります)に時間がかかります。
フォームの用意から表示は一連になります。

「Button1_Clickから制御を戻さない限り,RunWorkerCompletedイベントは発生しない」という点が最も気になります。
であれば並行処理が結構制限されると言うことになりますから。
ちなみに、デリゲートでやると同じ流れで問題が発生しません。


かまんべーる  2011-10-28 01:48:18  No: 147385

調べてみたのですが、BackGroundWorkerでは処理分岐後に合流するという手順は難しそうですね。
デリゲートも可能ではあるけど適切では無いみたいですね。
合流が必要ならThreading.Threadを使用するしかなさそうな。

私だけが作成するならなんとでもするのですが、共同で開発するので使いやすそうなBackGroundWorkerで出来れば良いのですが。


YuO  2011-10-28 03:39:33  No: 147386

> コントロールの数が多い、フォームの大きさに合わせて調整されるコントロールなどがあるという点からフォームの用意(フォームの大きさに合わせるという点からLOADとは異なります)に時間がかかります。
> フォームの用意から表示は一連になります。

その調整,本当に要りますか。
Anchor, Dock, TableLayoutPanelなどで実は済みませんか。

さらに,Resizeイベントで処理する方が適当だったりしませんか。

> 「Button1_Clickから制御を戻さない限り,RunWorkerCompletedイベントは発生しない」という点が最も気になります。
> であれば並行処理が結構制限されると言うことになりますから。

DoWorkが実行の本体,RunWorkerCompletedは実行結果をUIに反映させるためのものです。
UIに反映させるためには,UIスレッドで実行する必要があります。
故に,RunWorkerCompletedイベントはUIスレッドの制御がシステムに戻っているタイミングで実行されます。
# ちなみに,ProgressChangedイベントは途中の状況をUIに反映させるためのものです。

> 調べてみたのですが、BackGroundWorkerでは処理分岐後に合流するという手順は難しそうですね。

あくまで重い処理をUI側から簡単にワーカースレッドで処理させるために使うのがBackgroundWorkerです。
並列処理したければPLINQなんかがありますし,並行でもTask使ってしまえばよいです。
ただ,どちらにしてもUIをブロックしないために最初にBackgroundWorkerなりTaskなりでワーカースレッドに委譲する必要がありますが。

> 合流が必要ならThreading.Threadを使用するしかなさそうな。

今回の場合だと,RunWorkerCompletedイベントを適切に処理するだけで十分だと思います。

UIスレッドはイベントハンドラが呼ばれた後可能な限り素早くシステムに返すのが基本なので,
UIスレッドで待つような必要が生じたなら,基本的に処理を見直す必要があります。

Threadクラスが必要になるのはCOMのアパートメントモデルを指定する必要がある時くらいしか思いつきません。
スレッドプールを使わないので,大体の場合は効率が落ちますしメモリも食いますし……。


かまんべーる  2011-10-28 19:11:24  No: 147387

>その調整,本当に要りますか。
必要なんです。
resizeイベントが発生するからこそ必要になってくるんですね。

いろいろアドバイスありがとうございました。
全てを参考にして、もうちょっと考えてみます。


魔界の仮面弁士  2011-10-29 03:11:20  No: 147388

# 以下、求める回答になっているかどうかは分かりませんが。

> resizeイベントが発生するからこそ必要になってくるんですね。
その「調整」に長い時間(数ミリ秒以上)を要するのであれば、
そもそもの作り方を変える必要があるかもしれません。

Resize イベントというものは、ドラッグ座標の変化に応じて
何回も続けて呼び出されるものですから、個々の処理時間が長い場合、
リサイズ操作や再描画が阻害されてしまうことになります。

そして、「リサイズ処理」や「スクロール時の描画処理」が重い場合は、
単にスレッドを分けただけでは問題を解決できないことが少なくありません。
これは、座標計算等はワーカースレッドでも行えるものの、最終的な
画面描画処理は、結局 UI スレッドが担当するしかないためです。

改修案としては、たとえばリサイズ中には描画処理を中止してしまい、
リサイズ完了後に描画しなおすことで、リサイズのパフォーマンスを
向上させるといった方法があります。

もしもリサイズ中にも画面を表示させたい場合には、リサイズ中は
内容を簡素化させたり計算精度を低下させることで高速に描画できるようにし、
リサイズ完了後に正確な内容で再描画することで対応するといった手が使われます。

たとえばリアルタイム表示のグラフなどの場合は、リサイズ中はデータを蓄えるようにし、
リサイズ完了後に、溜めておいたデータをまとめて描画するといった手法をとります。

以下、参考資料として

DOBON.NET プログラミング道「フォームのリサイズが終了するまでコントロールの大きさを変えない」
http://dobon.net/vb/dotnet/form/preventcontrolresize.html

TechEd 2004 「T5-407」
http://www.masatohirai.com/GeniusSite/Showsrus

ステップ 7 ハンズオン シリーズ「Windows フォームにおけるパフォーマンスの向上」
http://www.microsoft.com/japan/seminar/msdn/step7/performanceup/control/play.aspx
http://www.microsoft.com/japan/seminar/msdn/step7/performanceup/asynchronous/play.aspx


※返信する前に利用規約をご確認ください。








  このエントリーをはてなブックマークに追加