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

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

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

BackgroundWorker複数実行時のキャンセル方法

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

■93972 / inTopicNo.1)  BackgroundWorker複数実行時のキャンセル方法
  
□投稿者/ ゆのじ (1回)-(2020/02/28(Fri) 00:10:33)

分類:[VB.NET/VB2005 以降] 

開発環境:Visual Studio 2017 Community

同一の処理を行うBackgroundWorkerを複数使った並列処理を実現したいと思い、以下のようなコード(要点のみ)で実装を行いましたが、
並列処理の停止がうまくできず困っています。
ButtonStartを押すと指定した並列数で処理を開始し、ButtonStopを押すと途中でその処理をキャンセルするものです。

   Dim bg() As System.ComponentModel.BackgroundWorker '並列処理を行うためのBackgroundWorkerのインスタンス

   Private Sub ButtonStart_Click(sender As Object, e As EventArgs) Handles ButtonStart.Click

        Dim numThread As Integer '並列処理の同時実行数
        Dim bgParameter As Parameter 'BackgroundWorkerに渡す引数

        '既にBackgroundWorkerが動いていた場合は何もしない
        If(bg IsNot Nothing) Then
            For i As Integer = 0 To bg.GetUpperbound(0)
                If(bg(i).IsBusy = True) Then
                    Exit Sub
                End If
            Next
        End If

        numThread = 値を決定する処理
  
        '指定した数だけBackgroundWorkerのインスタンスを生成
        ReDim bg(numThread - 1)
        For i As Integer = 0 To numThread - 1
            'bgで実行する処理の規定
            bg(i) = New System.ComponentModel.BackgroundWorker
            AddHandler bg(i).DoWork, AddressOf BackgroundWorker1_DoWork
            AddHandler bg(i).RunWorkerCompleted, AddressOf BackgroundWorker1_RunWorkerCompleted

            'bg(i)を中止可能にする
            bg(i).WorkerSupportsCancellation = True

            'パラメータをセットしてBackgroundWorkerを起動
            bgParameter = 値を決定する処理
            bg(i).RunWorkerAsync(bgParameter)
        Next

   End Sub

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

        For i as Integer = 0 To 繰り返しのループ数
            If(BackgroundWorker1.CancellationPending = True) Then
                e.Cancel = True
                Exit For
            End If

            何か重いループ処理
        Next

   End Sub

   Private Sub ButtonStop_Click(sender As Object, e As EventArgs) Handles ButtonStop.Click
        If (bg IsNot Nothing) Then
            For i As Integer = 0 To bg.GetUpperBound(0)
                If (bg(i).IsBusy = True) Then
                    bg(i).CancelAsync()
                End If
            Next
        End If
   End Sub


実際にプログラムを走らせてみると並列処理そのものはうまく動作するのですが、ButtonStopをクリックしてもBackgroundWorkerが停止せず最後まで進んでしまいます。
BackgroundWorkerをキャンセル可能にする際の注意点としては

・WorkerSupportsCancellationをTrueにする
・DoWorkの中で定期的にCancellationPendingプロパティをチェックし、それがTrueになっていたらDoWorkを終了させるようにコードを書く
・終了させたい場合はCancelAsyncを使う

のように理解しており、一応その注意点は守っているつもりなのですが・・・。
今回のようなBackgroundWorkerを複数使う並列処理の場合はまた何か注意すべき点があるのでしょうか?

本来であれば今は古いBackgroundWorkerではなくAsync/Await等の新しい手法を使うべきなのだとは思いますが、基本的な使い方が理解できていないのでBackgroundWorkerで何とかしたいと思っています。
ご教示いただければ幸いです。

引用返信 編集キー/
■93974 / inTopicNo.2)  Re[1]: BackgroundWorker複数実行時のキャンセル方法
□投稿者/ 魔界の仮面弁士 (2575回)-(2020/02/28(Fri) 02:06:30)
No93972 (ゆのじ さん) に返信
> 開発環境:Visual Studio 2017 Community

今回の質問内容からは外れてしまいますが、2017 の場合は BackgroundWorker 以外の選択肢もありますね。
Task にすることも検討してみてはいかがでしょう。
https://www.atmarkit.co.jp/fdotnet/chushin/masterasync_01/masterasync_01_02.html
https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/how-to-cancel-a-task-and-its-children
https://docs.microsoft.com/ja-jp/dotnet/standard/parallel-programming/how-to-cancel-a-parallel-for-or-foreach-loop



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

Handles 句が付与されていますが、AddHandler で動的に割り当てる設計なのであれば、
WithEvents BackgroundWorker1 は不要なのでは。(デザイン時に貼っておく意味はないように思えます)



> If(BackgroundWorker1.CancellationPending = True) Then

これは明らかに不自然です。

「bg(i) = New System.ComponentModel.BackgroundWorker()」で生成した BackgroundWorker と
デザイン時に貼り付けておいた BackgroundWorker1 は明らかに別のインスタンスなのですから、
ここは「If DirectCast(sender, BackgroundWorker).CancellationPending Then」じゃないかな…。
引用返信 編集キー/
■93975 / inTopicNo.3)  Re[2]: BackgroundWorker複数実行時のキャンセル方法
□投稿者/ ゆのじ (2回)-(2020/02/28(Fri) 07:23:45)
No93974 (魔界の仮面弁士 さん) に返信

早速のレスありがとうございます。

> ■No93972 (ゆのじ さん) に返信
>>開発環境:Visual Studio 2017 Community
>
> 今回の質問内容からは外れてしまいますが、2017 の場合は BackgroundWorker 以外の選択肢もありますね。
> Task にすることも検討してみてはいかがでしょう。

いずれはそうしたいと思っているのですが、時間的な制約等もあり今回は基本的な使い方がわかっているBackgroundWorkerでいきたいと考えています。

>>Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
>
> Handles 句が付与されていますが、AddHandler で動的に割り当てる設計なのであれば、
> WithEvents BackgroundWorker1 は不要なのでは。(デザイン時に貼っておく意味はないように思えます)

すみません、この書き方は同一の処理を行うBackgroundWorkerを複数同時に使用する方法を探していて別の掲示板に投稿されていたものなので、
AddHandlerの意味を正確に理解していませんでした。
AddHandlerでbgのDoWorkをBackgroundWorker1_DoWorkに動的に紐づけるのでHandlesの部分は不要なんですね。

>
>>If(BackgroundWorker1.CancellationPending = True) Then
>
> これは明らかに不自然です。
>
> 「bg(i) = New System.ComponentModel.BackgroundWorker()」で生成した BackgroundWorker と
> デザイン時に貼り付けておいた BackgroundWorker1 は明らかに別のインスタンスなのですから、
> ここは「If DirectCast(sender, BackgroundWorker).CancellationPending Then」じゃないかな…。

キャンセル動作が発生しないのはこれが原因だったようです。
コードを

If ((DirectCast(sender, System.ComponentModel.BackgroundWorker)).CancellationPending = True) Then
  e.Cancel = True
  Exit For
End If

と変更したところキャンセル動作をしてくれました。
最初コードを変更しただけだとキャンセル動作時にSystem.Reflection.TargetInvocationExceptionが発生してうまくいかなかったのですが、
「BackgroundWorkerをキャンセルした場合、RunCompletedで実行結果を取得しようとするとSystem.Reflection.TargetInvocationExceptionが発生する」
ということを知り、e.cancelledプロパティの値をチェックして実行結果の取得の有無を分けたところ正常に動作するようになりました。
なぜDoWork内でe.Cancelの値をセットする必要があるのかわからずおまじないのようにコードを書いていたのですが、理由がわかってスッキリしました。
どうもありがとうございました。

解決済み
引用返信 編集キー/

このトピックをツリーで一括表示


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

このトピックに書きこむ