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

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

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

Re[6]: 複数のBackgroundWorkerが1つしか実行されない


(過去ログ 177 を表示中)

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

■101706 / inTopicNo.1)  複数のBackgroundWorkerが1つしか実行されない
  
□投稿者/ ゆのじ (1回)-(2023/04/08(Sat) 17:31:35)

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

2023/04/08(Sat) 17:37:30 編集(投稿者)
2023/04/08(Sat) 17:36:31 編集(投稿者)

今更ながらVB.NETのBackgroundWorkerについての質問なのですが、同一の処理に対し異なる初期パラメータを与えて
それぞれの結果を得るプログラムをVB.NET(Visual Studio 2017)で作成しています。
個々のケースは互いに独立していて完全に並列化が可能なので高速化を図ろうと思い、BackgroundWorkerを同時に
複数個実行させて並列計算を行おうとしました(今更BackgroundWorkerなのはそれ以降の非同期/並列計算処理について
使い方を習得できていないためです)。

そこで以下のような形でプログラムを組んだのですが、BackgroundWorkerが1つしか実行されず困っています。
(コードは簡略化して要点のみ記してあります)
同一処理を行うBackgroundWorkerのインスタンスを指定した数だけ生成し、それぞれにパラメータを与えて実行させるものです。
なおメインの処理では別のクラスファイルに記述された非常に大がかりなFunctionプロシージャ(MainFunction)を用いて
処理を行っています(MainFunctionは引数を与えてから結果が返ってくるのに数秒〜数十秒かかる)。

Dim SimResult() As userDefinedSimResult ←結果を受け取るための構造体

Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
     Dim bg() As System.ComponentModel.BackgroundWorker
     Dim NumParallel As Integer
     Dim SimParam() As UserDefinedSimParameter ←BackgroundWorkerに引数を渡すための構造体

     NumParallell = ●● ←ユーザー入力により並列数を指定
     ReDim bg(NumParallel - 1)
     ReDim SimParam(bg.Getupperbound(0)), SimResult(bg.Getupperbound(0))

     For i As Integer = 0 To bg.Getupperbound(0)
          '=====この間で=====
          SimParam(i) = 〜〜
          SimParam(i).idx_bg = i
          '=====SimParam(i)の値を決定=====

          bg(i) = New As System.ComponentModel.BackgroundWorker
          AddHandler bg(i).DoWork, AddressOf MainRoutine_DoWork
          AddHandler bg(i).ProgressChanged, AddressOf MainRoutine_ProgressChanged
          AddHandler bg(i).RunWorkerCompleted, AddressOf mainRoutine_RunWorkerCompleted
          bg(i).RunWorkerAsync(SimParam(i))
     Next
End Sub

Private Sub MainRoutine_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
     Dim SimParamInBg As UserDefinedSimParameter
     Dim SimResultInBg As UserDefinedSimResult

     SimParamInBg = DirectCast(e.Argument, UserDefinedSimParameter)
     SimResultInBg = CM.MainFunction(SimParamInBg) ←メインの計算処理(非常に重い)

     e.Result = SimResultInBg
End Sub

Private Sub MainRoutine_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs)
     Dim TempSimResult As UserDefinedSimResut

     If(e.canceled = False) Then
         TempSimResult = DirectCast(e.Result, UserDefinedSimResult)
          SimResult(TempSimResult.idx_bg) = TempSimResult
     End If
End Sub

以下、別のクラスファイルに記述されたメイン処理
Public shared Function MainFunction(Param As UserDefinedSimParameter) As UserDefinedSimResult
     Dim Result As UserDefinedSimResult

     '=====ここから非常に重い処理=====
     Result = 〜〜
     '=====ここまで非常に重い処理=====

     Return Result
End Function

といった具合です。
実際に実行してみると複数生成したBackgroundWorkerのうち1つは動作していて値を返してくるのですが、その他はビジー状態に
なっているようでいくら待っても結果が返ってこず、NumParallel = 1にして並列計算を行わないようにすると正常に動作します。
試しにMainRoutine_DoWorkの中身を

Private Sub MainRoutine_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
     Dim Test As Integer
     Const CONSTANT_MAXIMUM As Integer = ●●

     Do
          Test = Test + 1
     Loop Until(Test > CONSTANT_MAXIMUM)

     e.Result = Test
End Sub

といった、他所の共通のFunctionプロシージャを呼び出さない別の処理にしてみたところ問題なく全てのBackgroundWorkerが実行されたので、
処理に時間のかかるFunctionプロシージャを複数のBackgroundWorkerから同時に使おうとして問題が起こっているのではと考えています。
(これまでも同様の実装で並列計算のプログラムを作ったことは何度かありましたが、その際にはDoWorkの中で共通のFunctionプロシージャといっても
一瞬で計算が終了する小さなものばかりでした)
もしこちらの想像通りだとすると、このような形での並列計算の実装は諦めるしかないのでしょうか?
それとも複数のBackgroundWorkerからMainFunctionを同時に使おうとすること自体は問題なく、何か他の原因で問題が起きているのでしょうか?
皆様のご教示をいただければと思います。

引用返信 編集キー/
■101707 / inTopicNo.2)  Re[1]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ WebSurfer (2657回)-(2023/04/08(Sat) 18:21:57)
No101706 (ゆのじ さん) に返信

> 今更ながらVB.NETのBackgroundWorkerについての質問なのですが、

.NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For
とか Parallel.Invoke とか) を利用するというというのは選択肢にないのですか?
引用返信 編集キー/
■101708 / inTopicNo.3)  Re[2]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ Azulean (1267回)-(2023/04/08(Sat) 18:56:42)
2023/04/08(Sat) 18:57:22 編集(投稿者)

No101707 (WebSurfer さん) に返信
> .NET Framework 4.0 で導入されたタスク並列ライブラリ (TPL・・・Parallel.For
> とか Parallel.Invoke とか) を利用するというというのは選択肢にないのですか?

「(今更BackgroundWorkerなのはそれ以降の非同期/並列計算処理について使い方を習得できていないためです)。」と書いているので、候補から意図的に外しているということでしょう。



No101706 (ゆのじ さん) に返信
> 実際に実行してみると複数生成したBackgroundWorkerのうち1つは動作していて値を返してくるのですが、その他はビジー状態に
> なっているようでいくら待っても結果が返ってこず、NumParallel = 1にして並列計算を行わないようにすると正常に動作します。

実行中に一時停止させて、それぞれのスレッドがどこを実行している、あるいは待機しているのか見極めてみてください。


> もしこちらの想像通りだとすると、このような形での並列計算の実装は諦めるしかないのでしょうか?
> それとも複数のBackgroundWorkerからMainFunctionを同時に使おうとすること自体は問題なく、何か他の原因で問題が起きているのでしょうか?
> 皆様のご教示をいただければと思います。

前述したように、デバッガで一時停止させてスレッドの状態を確認するのが先決でしょう。
いろいろな予想・仮説を立てるよりも、事実を確かめる方が有力な手がかりとなるため。


ありがちなのは以下でしょうか?

・その MainFunction 内の処理時間がかかるところでまとめて Invoke してメインスレッドに処理をさせている。(並列化の意味がない)
・その MainFunction 内で排他処理が入っている。(並列できないように作り上げている)
引用返信 編集キー/
■101710 / inTopicNo.4)  Re[3]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ ゆのじ (2回)-(2023/04/08(Sat) 19:21:25)
2023/04/08(Sat) 19:47:21 編集(投稿者)
2023/04/08(Sat) 19:43:26 編集(投稿者)
2023/04/08(Sat) 19:37:32 編集(投稿者)
2023/04/08(Sat) 19:37:29 編集(投稿者)

早速の返信ありがとうございます。

No101707 (WebSurfer さん) に返信
Azulean様の仰る通りです。
Parallel.Forなども調べてみたのですが、作業時間が限られていて新たな手法に習熟している時間がないため、以前に作った経験のある
BackgroundWorkerによる並列処理で組もうとしているところです。
時間があればそう言った方法にもトライしてみたいところなのですが、時間的な制約がシビアだったため見送りました。

No101708 (Azulean さん) に返信
デバッグ中の一時中断についてはMainFunctionの中にブレークポイントを入れてみたり、「デバッグ」→「すべて中断」で実行を中断させたりしてみたのですが、
ブレークポイントを入れても唯一値を返してくるインスタンス以外のBackgroundWorkerは引っかからず、「すべて中断」では停止場所がよく分からない
(メインフォームからサブフォームをForm2.Showdialog()のようにして呼び出してサブフォームの中で上記の処理を行っているのですが、「すべて中断」でも
Form2.ShowDialog()の部分が緑色になるだけで、肝心のMainFunctionのどこで止まっているのかが分からない)状態です。
CPU使用率自体は1つのBackgroundWorkerが処理を終えて結果を返してきても依然として高いままなので、他のBackgroundWorkerも何らかの処理を
行っていることは間違いないとは思うのですが・・・

ちなみに

>・その MainFunction 内の処理時間がかかるところでまとめて Invoke してメインスレッドに処理をさせている。(並列化の意味がない)
>・その MainFunction 内で排他処理が入っている。(並列できないように作り上げている)

については行っていないと思います(Invokeや排他制御について使い方を知らないため、そのような処理を行うコードを書いていない)。
メインスレッドとのやり取りはMainRoutine_DoWork内から処理の節目にProgressChangedイベントを呼んで進捗状況を通知するくらいですが、
このProgressChangedも通知してくるのは最終的に結果を返してくる1スレッドのみで、他のスレッドからは呼び出されていないようです。
(MainRoutine_DoWorkはProgressChangedを使ってメインスレッドとやり取りできるが、そこから更にFunctionプロシージャとして呼び出されている
MainFunctionの中からメインスレッドにデータを渡せるとは思っていないので、MainFunction中にはそのような処理は何も記述していない)

Azulean様の書き込みからすると、上記MainFunctionを複数のBackgroundWorkerから同時に使うこと自体は問題ではないということでしょうか?
デバッガを上手く使えてないようですので、MainFunctionのどこで止まっているか確認する方法があればご教示いただければ幸いです。
引用返信 編集キー/
■101712 / inTopicNo.5)  Re[4]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ 伝説のカレー (85回)-(2023/04/08(Sat) 20:01:36)
No101710 (ゆのじ さん) に返信

[並列スタック] ウィンドウでスレッドを表示する
https://learn.microsoft.com/ja-jp/visualstudio/debugger/using-the-parallel-stacks-window?view=vs-2022

スレッドを選択してスタックトレースが見ればいんじゃないですかね

処理が重いことだけが原因だとするならばアッカーマン関数のような計算コストの高い処理で
原因の切り分けはできそうな気がします

MainRoutine_DoWorkの処理を書き換えて問題ないならMainFunctionがやはり怪しい気がしますけどね

引用返信 編集キー/
■101713 / inTopicNo.6)  Re[4]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ WebSurfer (2658回)-(2023/04/08(Sat) 21:02:53)
No101710 (ゆのじ さん) に返信

> ■No101707 (WebSurfer さん) に返信
> 時間があればそう言った方法にもトライしてみたいところなのですが、
> 時間的な制約がシビアだったため見送りました。

そうですか、かえって時間と労量の節約になるのではないかと思ったのですが・・・

質問者さんのコードがどう動くのか分かりませんが、もし、下の図の A のようになる
とすると、OS がスレッドを切り替えて処理を行うのでそのオーバーヘッドの分逆に遅
くなるということになります。

https://image.itmedia.co.jp/ait/articles/0503/12/dt-mthread01_03.gif

なので、マルチスレッドアプリで処置時間の短縮を図るなら B のようにマルチコアを
利用できる環境が必要で、さらにマルチコアを有効に利用するプログラミングを行うと
いう話になると思います。

タスク並列ライブラリ (TPL) は、Microsoft のドキュメントによると、

"TPL は、使用可能なすべてのプロセッサを最も効率的に使用するように、コンカレン
シーの程度を動的に拡大します"

・・・とのことです。

使い方も、BackgroundWorker を使うよりは簡単だと思います。

そう言われてもやる気はないということなら自分は撤退しますが・・・

引用返信 編集キー/
■101714 / inTopicNo.7)  Re[5]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ ゆのじ (3回)-(2023/04/08(Sat) 22:01:48)
No101712 (伝説のカレー さん) に返信
ありがとうございます。このような機能があることは知りませんでした。
過去に自宅で同様の実装で作った別の重い並列処理プログラムのプロジェクトで試してみたところ、「すべて中断」で一時停止させた後
並列スタックでスレッドごとに中断行を確認できました。
プログラムが正常に動作している状態で「すべて中断」で一時停止させると全てのスレッドが別のクラスファイルに記述した共通の
Functionプロシージャ内で停止していたので、複数のスレッドからMainFunctionを同時に使うこと自体は問題ない気がしてきました。

元のプログラムは職場で開発しており今すぐには試せないので、1〜2日中には試してみて結果を報告したいと思います。
応答を返してこないスレッドがコードの何行目で停止しているかが同じ方法で分かれば問題解決のきっかけになりそうです。

No101713 (WebSurfer さん) に返信
頂いた図で言えば3番になりますので、特にシングルコアで複数スレッドを切り替えるようなオーバーヘッドはないと思います。
同じ実装で作った過去のプログラムでは32コア64スレッドのRyzen Threadripper 3970Xの環境上で並列計算しない場合と比較して
64並列時で20倍ほどの高速化が得られましたので、現在の自分の知識で効果が見込める方法としてBackgroundWorkerを選択しました。

Parallel.Forについては過去に少し調べたことがあり、単に使うだけならかなり簡単そうだとは思ったのですが、ワーカースレッドから
実行状況をメインスレッドへ通知する方法、メインスレッドを停めずにループ内を実行する方法等細部で分からない箇所がいくつかあり、
時間的な制約があるなかで(3〜4日以内には区切りをつけ結果を得られる状態に持っていく必要がある)それを調べて理解する時間は
残っていないと判断し見送りました。
アドバイスをいただいておいて申し訳ありませんが、今回はBackgroundWorkerを使用し、それがダメであれば並列化は諦めて
1スレッドでの順次実行という形をとりたいと思います。
ただ1スレッドだとかなり計算に時間がかかってしまうので、できれば今回実装した並列化の形は活かしたいところではあります。
引用返信 編集キー/
■101716 / inTopicNo.8)  Re[4]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ Azulean (1268回)-(2023/04/09(Sun) 08:18:47)
No101710 (ゆのじ さん) に返信
> ブレークポイントを入れても唯一値を返してくるインスタンス以外のBackgroundWorkerは引っかからず、「すべて中断」では停止場所がよく分からない
> (メインフォームからサブフォームをForm2.Showdialog()のようにして呼び出してサブフォームの中で上記の処理を行っているのですが、「すべて中断」でも
> Form2.ShowDialog()の部分が緑色になるだけで、肝心のMainFunctionのどこで止まっているのかが分からない)状態です。

すでにアドバイスがあるように別スレッドの状態も観察できる機能(並列スタックなど)がありますので、ぜひ活用してください。


> CPU使用率自体は1つのBackgroundWorkerが処理を終えて結果を返してきても依然として高いままなので、他のBackgroundWorkerも何らかの処理を
> 行っていることは間違いないとは思うのですが・・・

「高いまま」というのは、複数のコアがそれぞれ 100% 近く稼働しているということですか?
タスクマネージャの CPU 使用率は全コアの合算なので、これが 90% 〜 100% という風になっているなら、すべてのコアが稼働していると言えそうですが…。


> >・その MainFunction 内の処理時間がかかるところでまとめて Invoke してメインスレッドに処理をさせている。(並列化の意味がない)
> >・その MainFunction 内で排他処理が入っている。(並列できないように作り上げている)
>
> については行っていないと思います(Invokeや排他制御について使い方を知らないため、そのような処理を行うコードを書いていない)。

直接書いていなくても、既存の資産を利用している、外部ライブラリを利用しているなどあれば、該当する可能性は出てきますね。
このほかに、STA の COM を使っている場合もメインスレッドに処理が振られることはあります。


> Azulean様の書き込みからすると、上記MainFunctionを複数のBackgroundWorkerから同時に使うこと自体は問題ではないということでしょうか?
> デバッガを上手く使えてないようですので、MainFunctionのどこで止まっているか確認する方法があればご教示いただければ幸いです。

この擬似コードにおける表面的な部分で、問題は見えません。
中身が何か悪い、中身が排他する・同時実行できない性質を持つ可能性が高いです。

職場のコードであれば開示できないと思いますので、ご自身で並列スレッドにおけるデバッグ技能を磨き、スレッド間排他になり得る要素、あるいは必ずメインスレッドで処理させてしまう要素を見つけるしかありません。



No101714 (ゆのじ さん) に返信
> アドバイスをいただいておいて申し訳ありませんが、今回はBackgroundWorkerを使用し、それがダメであれば並列化は諦めて
> 1スレッドでの順次実行という形をとりたいと思います。
> ただ1スレッドだとかなり計算に時間がかかってしまうので、できれば今回実装した並列化の形は活かしたいところではあります。

気にしなくて良いと思います。
今回の質問における「問題の本質」は、Parallel.For でも Task でも起きる話である可能性は高く、本質の解決に当てる時間を優先するべきでしょうから。
引用返信 編集キー/
■101717 / inTopicNo.9)  Re[5]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ ゆのじ (4回)-(2023/04/09(Sun) 09:56:28)
No101716 (Azulean さん) に返信

>「高いまま」というのは、複数のコアがそれぞれ 100% 近く稼働しているということですか?
>タスクマネージャの CPU 使用率は全コアの合算なので、これが 90% 〜 100% という風になっているなら、すべてのコアが稼働していると言えそうですが…。

その通りです。
職場のPCは8コア16スレッドのシステムなので、並列数を16にすると全てのCPUの使用率がほぼ100%になります(グラフの表示を変更して個々の論理CPUの
使用率が見える状態で確認しました)。
1つのスレッドが結果を返してきて計算が終了するとそこだけ使用率が下がりますが他の論理CPUは高い使用率がずっと継続している状態です。

>直接書いていなくても、既存の資産を利用している、外部ライブラリを利用しているなどあれば、該当する可能性は出てきますね。

外部ライブラリとしてはDXライブラリを用いていて、MainFunctionの内部ではそれが提供する構造体型の変数を演算に用いていますが、DXライブラリが提供する
関数自体はMainFunction内部では未使用です。
STAのCOMについては申し訳ありませんがよく分からないのですが、意図せず使っているということはあり得るでしょうか?
(プロジェクトのプロパティから追加した参照については上記のDXライブラリのみです)

いただいた返信の内容、自宅の別プロジェクトで試した並列スタックの結果等からすると、MainFunctionを複数スレッドから使おうとすることではなく、MainFunctionの
内部処理自体に問題がある可能性が高いということですね。
ここで教えていただいた並列スタックを使って確認してみたいと思います。
引用返信 編集キー/
■101718 / inTopicNo.10)  Re[6]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ WebSurfer (2659回)-(2023/04/09(Sun) 12:51:56)
No101714 (ゆのじ さん) に返信

撤退したと言いながらまたレスするのもなんですが、以下の点の対応を書いておきま
す。将来 TPL を使う機会があったら思い出していただければと思います。

> Parallel.Forについては過去に少し調べたことがあり、単に使うだけならかなり簡単
> そうだとは思ったのですが、ワーカースレッドから実行状況をメインスレッドへ通知
> する方法、メインスレッドを停めずにループ内を実行する方法等細部で分からない
> 箇所がいくつかあり、

(1) ワーカースレッドから実行状況をメインスレッドへ通知する方法

Progress<T> クラスを使ってはいかがですか? 具体例は以下の記事の「バックグラ
ウンドの処理から進捗を表示するには?」のセクションを見てください。

WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?
https://atmarkit.itmedia.co.jp/ait/articles/1512/02/news019.html


(2) メインスレッドを停めずにループ内を実行する方法

await Task.Run を用いてスレッドプールで Parallel.For を実行する。具体例は以下
の記事のコードの中の ParallelForProgress_Click メソッドを見てください。

try - catch で OperationCanceledException を捕捉できない
http://surferonwww.info/BlogEngine/post/2021/07/17/cancel-parallel-for-loop.aspx
引用返信 編集キー/
■101723 / inTopicNo.11)  Re[6]: 複数のBackgroundWorkerが1つしか実行されない
□投稿者/ ゆのじ (5回)-(2023/04/09(Sun) 22:23:34)
本日職場の該当プロジェクトで並列スタックを使ったところ無事に各スレッドの停止箇所を特定できました。
ご指摘のとおり問題はMainFunctionの内部の処理にあったようです。

MainFunctionの内部ではDo〜Loopでループ処理を行っている箇所があって、ループを抜け出すかどうかの判定として
特定のDouble型変数の値をチェックしているのですが、ビジー状態のスレッドでは該当する変数の値がNaNになっていたため
ループを抜け出すための条件を満たすことができず無限ループに陥っていたようです。
問題の箇所でNaNにならないよう処理を少し変えたところ、無事に全てのスレッドが正常に値を返してくるようになりました。

同じ演算をしているはずなのに特定の1スレッド以外で何故NaNが起きるのか等原因は完全には分かっていないのですが、
BackgroundWorker側の問題ではなかったようなのでひとまず解決とさせていただきます。
並列スタックについて教えてくださりありがとうございました。


No101718 (WebSurfer さん) に返信

ご丁寧にどうもありがとうございます。
いずれはTPL等も習得しなければと思っていましたので大変参考になりました。
時間ができましたら教えていただいたURLを見て勉強してみます。
解決済み
引用返信 編集キー/


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

このトピックに書きこむ

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

管理者用

- Child Tree -