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

わんくま同盟

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

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

ツリー一括表示

非同期&並列処理の実装方法について /ゆーすけ (25/02/15(Sat) 12:16) #103545
Re[1]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/15(Sat) 12:32) #103546
Re[1]: 非同期&並列処理の実装方法について /魔界の仮面弁士 (25/02/15(Sat) 18:20) #103550
│└ Re[2]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/15(Sat) 18:48) #103551
│  ├ Re[3]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/15(Sat) 19:20) #103552
│  └ Re[3]: 非同期&並列処理の実装方法について /魔界の仮面弁士 (25/02/16(Sun) 02:39) #103553
│    └ Re[4]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/16(Sun) 04:30) #103554
│      ├ Re[5]: 非同期&並列処理の実装方法について /魔界の仮面弁士 (25/02/16(Sun) 12:36) #103557
│      │└ Re[6]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/16(Sun) 15:35) #103559
│      └ Re[5]: 非同期&並列処理の実装方法について /魔界の仮面弁士 (25/02/16(Sun) 13:59) #103558
Re[1]: 非同期&並列処理の実装方法について /WebSurfer (25/02/16(Sun) 10:51) #103555
  └ Re[2]: 非同期&並列処理の実装方法について /ゆーすけ (25/02/16(Sun) 11:09) #103556


親記事 / ▼[ 103546 ] ▼[ 103550 ] ▼[ 103555 ]
■103545 / 親階層)  非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (1回)-(2025/02/15(Sat) 12:16:13)

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

現在VB.NET(Visual Studio 2022)を使って物理シミュレーションを行うプログラムを作ろうと考えていますが、
計算量が多いので並列化を使って高速化できないかと考えています。
考え方としては

・実際のシミュレーションでは同一の物理計算を異なる様々な初期条件で実行したいが、それを1つずつ処理していくと
時間がかかるので、初期条件の異なる複数のケースを並列計算で同時並行で計算したい
・そのため、物理計算用のユーザー定義関数や物理演算のコアとなるルーチンはフォームとは別のCMという名前の
クラスファイルを作ってそちらに記述
・並列数はユーザーが任意で指定し、指定した数だけワーカースレッドを起動し、個々のワーカースレッドからCMクラスの
物理計算用の関数やルーチンを呼び出して使用する

といったもので、使い方を把握しているBackgroundWorkerを使って以下のような実装で組んでみました。
(Task、Parallel.For等は使い方を習得する時間が十分にとれずまだ使えるようになっていないため古いBackgroundWorkerを使用)

・CMクラスの記述
Public Structure UserParameter 'メインスレッドからワーカースレッドに初期条件を渡すための構造体
  Dim idx As Integer '個々のワーカースレッドを識別するためのインデックス
  Dim ** As ** 'その他の計算に必要なパラメータ等
  等(メンバ変数にはその他配列やList等も含む)
End Structure
Public Structure SimulationResult 'ワーカースレッドの計算結果をメインスレッドに渡すための構造体
  Dim idx As Integer '個々のワーカースレッドを識別するためのインデックス
  Dim ** As **
  Dim **() As **
  等(メンバ変数にはその他配列やList等も含む)
End Structure

Public Function SolverMain(in_param As userParameter) As SimulationResult '物理計算のメイン部分
  Dim Result As SimlationResult

  '極めて重いループ処理(開始)
  Do
    〜
    Result = 〜
  Loop Until(終了条件)
  '極めて重いループ処理(終了)

  Return Result
End Function

・ユーザーフォーム
  Dim simPartResult() As CM.SimulationResult
  Dim numThread As Integer 'ユーザが指定するスレッド数
  Dim simParam() As CM.UserParameter '個々のワーカースレッドに与えるシミュレーション用の初期パラメータ
  Dim bg() As BackgroundWorker 'ワーカースレッドのインスタンス

  numThread = ** '何らかの方法(NumericUpDownからの値の取得等)でスレッド数を指定
  ReDim simParam(numThread - 1)
  ReDim bg(numThread - 1)
  ReDim simPartResult(numThread - 1)

  For i As Integer = 0 To numThread - 1
    bg(i) = New BackgroundWorker
    AddHandler bg(i).DoWork, AddressOf PhysicalEngine_DoWork
    AddHandler bg(i).RunworkerCompleted, AddressOf Physicalengine_RunWorkerCompleted

    'ワーカースレッドに渡す初期条件を決定する処理(開始)
    simParam(i).idx = i 'これから起動するワーカースレッドのインデックス番号を付与
    simParam(i).** = 〜〜
    simParam(i).** = 〜〜
    'ワーカースレッドに渡す初期条件を決定する処理(終了)

    bg(i).RunWorkerAsync(simParam(i))
  Next

・ユーザーフォームのBackgroundWorkerの処理部分
Private Sub PhysicalEngine_DoWork(sender As Object, e As Syste.ComponentModel.DoWorkEventArgs)
  Dim InternalParam As CM.UserParameter
  Dim InternalResult As CM.SimulationResult

  InternalParam = DirectCast(e.Argument, CM.UserParameter)
  InternalResult = CM.SolverMain(InternalParam)
  e.Result = InternalResult
End Sub

Private Sub PhysicalEngine_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs)
  Dim tmpResult As CM.SimulationResult

  tmpResult = DirectCast(e.Result, CM.SimulationResult)
  simPartResult(tmpResult.idx) = tmpResult

End Sub

長くなるので実際の物理計算部分は書けませんが、コードの枠組みとしてはおおよそこのような形となります。
以下続きます。

[ □ Tree ] 返信 編集キー/

▲[ 103545 ] / 返信無し
■103546 / 1階層)  Re[1]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (2回)-(2025/02/15(Sat) 12:32:38)
No103545 (ゆーすけ さん) に返信
以下続きです。

このような実装で実際にプログラムを組んで走らせたところ、動作自体は問題なく並列で動作し高速化も図れたのですが、
初期条件を変えずに計算を繰り返すと計算の度に結果が変動するという挙動になり困っています。
並列数を1に指定、即ちシングルスレッドで実行した場合はこのような挙動は起きず計算結果も毎回同じ値となります。

各ワーカースレッドやSolverMainからはグローバル変数にはアクセスしていませんが、CMクラス中にはSolverMainにおいて
頻繁に使用される物理計算用の関数(ベクトルの演算など)をSolverMainとは別のFunctionプロシージャとして多数定義しており、
このような「複数のワーカースレッドから1つのクラスに定義されたFunctionプロシージャを共用している」という構造がひょっとすると
計算結果が毎回狂う原因になっているのではないか?と疑っています。
(Visual C#用の並列計算プログラミングの解説書を買って読んだところ、変数を単純にインクリメントするだけのメソッドでさえ
スレッドセーフではなく、複数のワーカースレッドから呼び出されると計算結果が正しくなくなる場合があると書いてあったため)

仮にそうだとした場合、最初に書いたような並列計算による処理の分担及び高速化と物理演算部分の共有によるコードの簡素化を
両立させるためにはどのように実装すればよいでしょうか?
(SolverMain自体は非常に大規模で数千行のコードからなる大きく重い処理のため、排他制御を行うのは労力及びパフォーマンスの点から
非現実的ではないかと思っています)
何かよい方法があればご教示いただければ幸いです。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103545 ] / ▼[ 103551 ]
■103550 / 1階層)  Re[1]: 非同期&並列処理の実装方法について
□投稿者/ 魔界の仮面弁士 (3826回)-(2025/02/15(Sat) 18:20:14)
No103545 (ゆーすけ さん) に返信
> Task、Parallel.For等は使い方を習得する時間が十分にとれずまだ使えるようになっていない
その辺も、作業内容によって効率の良い処理のさせ方が変わってきますしね。
https://qiita.com/longlongago_k/items/8f19d84fce6dd677922e
https://csharptan.wordpress.com/2011/12/10/%e9%9d%9e%e5%90%8c%e6%9c%9fio%e5%be%85%e3%81%a1/



> Public Function SolverMain(in_param As userParameter) As SimulationResult '物理計算のメイン部分
>   Dim Result As SimlationResult
>
>   '極めて重いループ処理(開始)
>   Do
>     〜
>     Result = 〜
>   Loop Until(終了条件)
>   '極めて重いループ処理(終了)
>
>   Return Result
> End Function
実際の処理が本当に、これと同様の手順になっているのでしょうか?

そのイメージ コードだと、Result には「ループの最終回」の結果しか保持されないため、
それ以前のループ処理の結果が失われてしまうかと思うのですが…。

変数宣言時に初期値指定が書かれていないという事からして、ループの最後の部分が
  Result = DoAnything(Result, arg)
といった処理であったと考えるのも不自然ですし。

一応、途中経過をフィールド変数やプロパティに保持しているのだとすれば、
上記の手順でも各ループ回の処理結果を反映させられる可能性がありえますが、
ワーカースレッドのためにそのようなコードを書くとも思えなかったので、かなり違和感が。


> ・CMクラスの記述
CM は Module ではなく Class なのですよね? これも文法的な違和感があります。

というのも、SolverMain は Shared Function というわけでも無いのに、
BackgroundWorker の中で CM のインスタンスが生成されていないように見えるためです。


まずは CM クラスの公開メソッドがスレッドセーフになっているかどうかを確認してみてください。

・SolverMain の中で、ローカル変数以外の変数(フィールド変数)にアクセスしていないか?
・SolverMain で利用しているメソッド(自クラス/他クラス問わず)は、いずれもスレッドセーフであることが保証されているか?


No103546 (ゆーすけ さん) に返信
> (Visual C#用の並列計算プログラミングの解説書を買って読んだところ、変数を単純にインクリメントするだけのメソッドでさえ
> スレッドセーフではなく、複数のワーカースレッドから呼び出されると計算結果が正しくなくなる場合があると書いてあったため)

その通りですね。ワーカースレッドの中で、たとえば
 Me._Index += 1
とか
 Me._IsBusy = Not Me._IsBusy
とか
 If Me._lockObj Is Nothing Then Me._lockObj = New Object()
などといった記述を行うのは NG となります。


とはいえ上記は相手がフィールド変数である場合の話。
複数のスレッドから同時に読み書きされることのない変数であれば、インクリメントしても問題ありません。

また、フィールド変数へのアクセスであっても、読み取りを行うだけで値を変化させることが無いのであれば、
作業中に変化しないことが保証されているもの(ReadOnly 変数など)に限り、安全に読み取りできます。

そして複数のスレッドから参照される可能性があり、かつ、作業中に値を変化させる必要がある変数への
読み書きについては、排他制御や Interlocked クラスなどによるアトミック操作が必要となります。
たとえばインクリメントなら、Interlocked.Increment メソッドという物が用意されています。


> 物理計算用の関数(ベクトルの演算など)
そのベクトル演算は、自作処理でしょうか。
それとも、何らかのベクトル演算用のクラス/構造体のライブラリを利用しているのでしょうか。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103550 ] / ▼[ 103552 ] ▼[ 103553 ]
■103551 / 2階層)  Re[2]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (3回)-(2025/02/15(Sat) 18:48:52)
No103549 (魔界の仮面弁士 さん) に返信

魔界の仮面弁士様
早速の返信ありがとうございます。

> 実際の処理が本当に、これと同様の手順になっているのでしょうか?
>
> そのイメージ コードだと、Result には「ループの最終回」の結果しか保持されないため、
> それ以前のループ処理の結果が失われてしまうかと思うのですが…。
>
> 変数宣言時に初期値指定が書かれていないという事からして、ループの最後の部分が
>   Result = DoAnything(Result, arg)
> といった処理であったと考えるのも不自然ですし。
>
> 一応、途中経過をフィールド変数やプロパティに保持しているのだとすれば、
> 上記の手順でも各ループ回の処理結果を反映させられる可能性がありますが、
> ワーカースレッドでそういったコードを書くとは思えなかったので、かなり違和感が。

この物理シミュレーションは初期条件を与えた物体の運動を運動方程式を数値的に解くことでシミュレートして
時間ごとの位置や速さ等の状態を求めるもので、Doループを回すことでシミュレーション内の時間が進行します。
Resultは結果を保持するList形式の変数をメンバ変数として持っていて、Doループ内での計算結果はその都度
Listに追記され、Loop終了後にSolverMainの戻り値として返されます。
本題とは直接関係ない部分かなと判断し省略してしまいました、申し訳ありません。


> CM は Module ではなく Class なのですよね?
> これも文法的に違和感が…。
>
> というのも、SolverMain は Shared Function というわけでも無いのに、
> BackgroundWorker の中で CM のインスタンスが生成されていないように見えるためです。
>
>
> まずは CM クラスの公開メソッドがスレッドセーフになっているかどうかを確認してみてください。
>
> ・SolverMain の中で、ローカル変数以外の変数(フィールド変数)にアクセスしていないか?
> ・SolverMain で利用しているメソッド(自クラス/他クラス問わず)は、いずれもスレッドセーフであることが保証されているか?

申し訳ありません、CMの中のユーザー定義関数は全てPublic Sharedでした。
お恥ずかしながら自分はクラスとモジュールについてちゃんと理解してはおらず、共通して使うユーザー定義関数を
CMクラスにまとめたのも「プログラム全体で頻繁に使われるユーザー定義関数や物理定数、構造体は1ヶ所にまとめておいて
どのフォームからでも共通して呼び出せるようにしておこう」くらいの考えでした。
私の使い方だとクラスにする必要はなくモジュールとして記述すべきということでしょうか?

ちなみにCMクラス中にはフィールド変数(要はCMクラスの中で各プロシージャの外で宣言されている変数ですよね?)はありませんが、
SolverMain中で非常に高頻度で使う物理定数はCMの先頭においてPublic Constでフィールド定数(?)として宣言しています。
これは関係あったりするでしょうか?(作業中に変化しないので無関係?)

またSoverMain中のメソッドがスレッドセーフであることの確認ですが、これは私がCMクラス内に定義したSolverMainやその中で使用される
ユーザー定義関数だけではなく、それらの関数中で使用されている演算も含めて、という理解でよろしいでしょうか?
(たとえばA = A + B のような通常の加算処理など)
とするとどのように確認をすればいいかご教示いただければと思います。


> 複数のスレッドから同時に読み書きされることのない変数であれば、インクリメントしても問題ありませんし、
> フィールド変数へのアクセスがある場合でもあっても、
> 作業中に変化しないことが保証されているもの(ReadOnly 変数など)であれば大丈夫かと。
>
> 複数のスレッドから参照される可能性があり、作業中に値を変化させる必要がある変数への読み書きについては、
> 排他制御や Interlocked クラスなどによるアトミック操作が必要となります。
> たとえばインクリメントなら、Interlocked.Increment メソッドという物が用意されています。

CMクラス、またはモジュール中に記述されたSolverMainその他のFunctionプロシージャがプロシージャ内で宣言されたローカル変数か
引数としてプロシージャに渡された変数しか使っていない場合は、SolverMainが複数のスレッドから呼び出されても問題はないのでしょうか?

色々と拙い理解で申し訳ありませんが、どうぞよろしくお願いいたします。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103551 ] / 返信無し
■103552 / 3階層)  Re[3]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (4回)-(2025/02/15(Sat) 19:20:35)
2025/02/15(Sat) 22:30:44 編集(投稿者)
2025/02/15(Sat) 19:28:36 編集(投稿者)

No103549 (魔界の仮面弁士 さん) に返信

申し訳ありません、最後の一文を見落としていました。

>> 物理計算用の関数(ベクトルの演算など)
>そのベクトル演算は、自作処理でしょうか。
>それとも、何らかのベクトル演算用のクラス/構造体のライブラリを利用しているのでしょうか。

画面表示用にDXライブラリという3D表示用の外部ライブラリ(C#用ですがVB.NETでも使える)を使用していて、
そのライブラリが提供しているベクトル型の構造体をSolverMainその他で多用しています。
また、メインスレッドでは画面表示を行うためDXライブラリが提供する様々な関数を利用していますが、
ワーカースレッド及びそこから呼び出されるSolverMainではDXライブラリの提供する関数は一切使用せず、
ベクトル演算の処理自体は自作でコードを書いています(当初はライブラリが提供するベクトル演算用の
関数を利用していましたが、それだと呼び出しのオーバーヘッドが大きく逆に速度が低下したため)

ちなみにDXライブラリが提供するこのベクトル型の構造体はVB.NETのコードで書き表せば

Public Structure VECTOR_D
  Dim x As Double
  Dim y As Double
  Dim z As Double
End Structure

というシンプルなものです。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103551 ] / ▼[ 103554 ]
■103553 / 3階層)  Re[3]: 非同期&並列処理の実装方法について
□投稿者/ 魔界の仮面弁士 (3827回)-(2025/02/16(Sun) 02:39:52)
No103551 (ゆーすけ さん) に返信
> 本題とは直接関係ない部分かなと判断し省略してしまいました
全部書いても丸投げになってしまうので、判断は難しいですよね。
「現象を再現可能な最低限のコード」にまで絞り込めれば、問題点をより具体的に指摘できる可能性もありますが。


個々のデータはインデックスで識別されるようなので、自分なら UserParameter の各メンバーは ReadOnly にすると思いますし、
SimulationResult においても、少なくとも idx は ReadOnly にします。今回の問題には無関係な話ですけれどね。

配列で管理されている SimulationResult は、List を内包しているみたいですし、Structure ではなく Class にするかな…。
その最終結果を蓄積させる simPartResult も、配列にはせず、List(Of SimulationResult) もしくは
Dictionary(Of Integer, SimulationResult) で管理するかも知れません。まぁその辺は好みの問題。



閑話休題。


> 私の使い方だとクラスにする必要はなくモジュールとして記述すべきということでしょうか?
自分が Module を使うのは拡張メソッドを作る時ぐらいなので、個人的には「否」です。

元コードを見ていないため断言はできませんが、今回はフィールドが無いということですし、
Shared のままで良いと思いますよ。
単一のクラスで済ませるべきか、複数のクラスにわけるべきかは見てみないと分かりません。

引数と戻り値だけですべてが語れるメソッドであるならば、大抵は共有メソッドで済ませています。
ただし状態値を持ったオブジェクトとしておきたい場合、あえてインスタンス化できるクラスとして設計しておき、
そのインスタンスを DoWork 内で生成するようにする設計とすることもあります。その場合、
Singleton-class にすることもあったりして……まぁケースバイケースですね。

インスタンス不要な場合、 C# であれば静的クラス(static class)を採用したいところですが、
VB には Shared Class というものが無いので、せめてインスタンス化だけ封じています。

Partial Public NotInheritable Class CM
 Private Sub New()
 End Sub
End Class

機能的には Module でも良いとは思いますが、Module だと名前空間を明示せずにアクセスできてしまうことから、
個人的にはあまり好みません。「共通して使うもの」だからこそ、Namespace で明確に管理できた方が、
複数のプロジェクトから使う際にも、複数の共通ライブラリを併用しやすいと思っているもので。



> ちなみにCMクラス中にはフィールド変数(要はCMクラスの中で各プロシージャの外で宣言されている変数ですよね?)はありませんが、
> SolverMain中で非常に高頻度で使う物理定数はCMの先頭においてPublic Constでフィールド定数(?)として宣言しています。
> これは関係あったりするでしょうか?(作業中に変化しないので無関係?)
Constを使うことは問題ありません。名前付きリテラル扱いであり、値が変化することは無いですから。

一方、ReadOnly Field や ReadOnly Property だと、値を変更できる可能性が一応あります。

Class Compiler
 Shared ReadOnly Months() As Integer = {31,28,31,30,31,30,31,31,30,31,30,31}
 Shared Function Main() As Integer
  Console.WriteLine(Months(1))
  Months(1) = 29
  Console.WriteLine(Months(1))
  Return 0
 End Function
End Class


> またSoverMain中のメソッドがスレッドセーフであることの確認ですが、これは私がCMクラス内に定義したSolverMainやその中で使用される
> ユーザー定義関数だけではなく、それらの関数中で使用されている演算も含めて、という理解でよろしいでしょうか?
「他のスレッドから同時にアクセスされる変数が無い」のならばスレッドセーフです。
しかし「複数のスレッドで共有されている変数」があるのなら、演算部も含めて
それらがマルチスレッド対応であるかどうかを、常に意識してコーディングせねばなりません。

たとえば、「64bit 整数型(Long あるいは ULong)を読み取る」というだけの単純処理においても、
32bit CPU 環境では、上位32bitと下位32bitの読み取りが命令が発生するため、
「読み取り開始から読み取り完了までの間に、他のスレッドによって編集されていた」という可能性が起こりえるため、
読み取り処理を不可分とするために、わざわざ Interlocked.Read メソッドという専用命令が利用されています。
もちろん他のスレッドから同時にアクセスされることのない変数に対しては不要ですけれどね。


> (たとえばA = A + B のような通常の加算処理など)
変数 A と変数 B の両方がローカル変数な Integer 型であるのならば問題もありません。

しかし、それらの変数のいずれかを ByRef 引数のメソッドに渡していた場合には、
スレッドセーフではなくなる可能性がありえます。
(そのメソッド内で別スレッドが生成されており、引数の内容を書き換えてしまう場合など)


> CMクラス、またはモジュール中に記述されたSolverMainその他のFunctionプロシージャがプロシージャ内で宣言されたローカル変数か
> 引数としてプロシージャに渡された変数しか使っていない場合は、SolverMainが複数のスレッドから呼び出されても問題はないのでしょうか?

SolverMain がローカル変数しか使っていなかったとしても、その中で「他のメソッド」を呼び出しているなら、
それぞれのメソッドがスレッドセーフであるかどうかを確認する必要があるでしょう。

引数として渡された値も、たとえば
 BackgroundWorker1.RunWorkerAsync(arg(0))
 BackgroundWorker2.RunWorkerAsync(arg(1))
みたいな処理をしている場合、それぞれの処理が終わる前に、
呼び出し元から arg(0) や arg(1) を読み書きしてはいけないのは言わずもがな。

そういえば現状の実装では、「すべてのスレッドの処理が完了したのかどうか」を
どうやって判断しているのでしょうか。


> とするとどのように確認をすればいいかご教示いただければと思います。
値が「どこで書き込まれているか」「どこで読み取っているか」をきちんと把握した上で、
それらが「それらは複数のスレッドで同時にアクセスされうるのかどうか」を考えてコーディングします。

そのうえで、「そもそも同時に読み書きさせない」設計にするか、あるいは
「並列で読み書きできるよう排他制御を組み込む」ようにする、ということです。

自作処理であれば、同時アクセスの禁止については開発者自身が責任を持つしかありません。
既存のライブラリなら、ドキュメントの記載で判断するか、その型のソースコードを読み解くかです。

たとえば「配列」(System.Array)クラスのドキュメントに、「スレッド セーフ」という項がありますね。
https://learn.microsoft.com/ja-jp/dotnet/api/system.array?WT.mc_id=DT-MVP-8907&view=netframework-4.8.1#thread-safety
>>
>> この型のパブリック静的 (Visual Basic のShared) メンバーはスレッド セーフです。
>> インスタンス メンバーがスレッド セーフであるとは限りません。
>>
>> この実装では、Arrayの同期 (スレッド セーフ) ラッパーは提供されません。ただし、Array に基づく .NET クラスは、
>> SyncRoot プロパティを使用して独自の同期バージョンのコレクションを提供します。
>>
>> コレクションを列挙することは、本質的にスレッド セーフなプロシージャではありません。
>> コレクションが同期されている場合でも、他のスレッドはコレクションを変更できるため、列挙子は例外をスローします。
>> 列挙中のスレッド セーフを保証するには、列挙全体の間にコレクションをロックするか、
>> 他のスレッドによって行われた変更によって発生する例外をキャッチします。
>>


No103552 (ゆーすけ さん) に返信
> Public Structure VECTOR_D
>   Dim x As Double
>   Dim y As Double
>   Dim z As Double
> End Structure

その構造体が、それぞれのスレッドで個別管理される値(他のスレッドと共有されていないもの)ならば、
先に述べた通り、特に気にする必要はありません。

しかし、複数のスレッド(メインスレッドと作業スレッドなど)で共有されていた場合については、
「その構造体が同時にアクセスされるのかどうか」を常に考慮せねばなりません。
例に挙げられたインクリメントのように、「読み取りも書き込みも行われる」変数であれば、
SyncLock ステートメントなどを使って排他処理が必要になることもあろうかと思います。


あるいは 3元ベクトルという事は xyzの3値が揃って初めて意味を持つものであることから、
「演算によって 3値を書き換えている最中」に、別の処理で「ベクトル値を読み取る」ことが
無いようにしたい…という要件が必要になることもありえるかと。(マルチスレッドかどうかとは無関係に)

例として、x=0,y=0,z=0 が演算によって x=2,y=3,z=-1 に変わる処理があったとします。
現在の VECTOR_D の構造だと、「0,0,0」から「2,3,-1」へと変更している最中に、
その変数が読み取られると、編集途中の「2,3,0」という値が読み取られてしまう可能性がありえます。

そういったことを避けるため、複数メンバーを持つクラスや構造体においては、
『一部だけが書き換わった状態』で読み取られることを防ぐ設計にしておくこともあります。
たとえば、変更不能な immutable 型として実装するなど。

具体的には、複数メンバーの値をコンストラクタで一斉に指定できるようにし、
それぞれのメンバーは Public ReadOnly Property にしておいて、
演算については Shared メソッドや演算子オーバーロードを用いて、
演算結果を Return するという実装にする形というものです。

このパターンの実装例としては、複素数型 (System.Numerics.Complex 構造体)などがあります。
https://learn.microsoft.com/ja-jp/dotnet/api/system.numerics.complex?WT.mc_id=DT-MVP-8907&view=net-8.0

Complex 構造体のソースコードはこちら。
[.NET Framework 版] https://referencesource.microsoft.com/#q=System.Numerics.Complex
[.NET 版] https://source.dot.net/#q=System.Numerics.Complex
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103553 ] / ▼[ 103557 ] ▼[ 103558 ]
■103554 / 4階層)  Re[4]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (5回)-(2025/02/16(Sun) 04:30:38)
No103553 (魔界の仮面弁士 さん) に返信

詳細な返信ありがとうございます。
私の知識不足でなかなか理解しきれない部分も多くて申し訳ありません。


> ■No103551 (ゆーすけ さん) に返信
>>本題とは直接関係ない部分かなと判断し省略してしまいました
> 全部書いても丸投げになってしまうので、判断は難しいですよね。
> 「現象を再現可能な最低限のコード」にまで絞り込めれば、問題点をより具体的に指摘できる可能性もありますが。
>
>
> 個々のデータはインデックスで識別されるようなので、自分なら UserParameter の各メンバーは ReadOnly にすると思いますし、
> SimulationResult においても、少なくとも idx は ReadOnly にします。今回の問題には無関係な話ですけれどね。
>
> 配列で管理されている SimulationResult は、List を内包しているみたいですし、Structure ではなく Class にするかな…。
> その最終結果を蓄積させる simPartResult も、配列にはせず、List(Of SimulationResult) もしくは
> Dictionary(Of Integer, SimulationResult) で管理するかも知れません。まぁその辺は好みの問題。

クラスの使い方がなかなかきちんと理解できておらず、複数のパラメータはとりあえず構造体でひとまとめにして渡す、
という実装しか思いつかないもので・・・


>>私の使い方だとクラスにする必要はなくモジュールとして記述すべきということでしょうか?
> 自分が Module を使うのは拡張メソッドを作る時ぐらいなので、個人的には「否」です。
>
> 元コードを見ていないため断言はできませんが、今回はフィールドが無いということですし、
> Shared のままで良いと思いますよ。
> 単一のクラスで済ませるべきか、複数のクラスにわけるべきかは見てみないと分かりません。
>
> 引数と戻り値だけですべてが語れるメソッドであるならば、大抵は共有メソッドで済ませています。
> ただし状態値を持ったオブジェクトとしておきたい場合、あえてインスタンス化できるクラスとして設計しておき、
> そのインスタンスを DoWork 内で生成するようにする設計とすることもあります。その場合、
> Singleton-class にすることもあったりして……まぁケースバイケースですね。
>
> インスタンス不要な場合、 C# であれば静的クラス(static class)を採用したいところですが、
> VB には Shared Class というものが無いので、せめてインスタンス化だけ封じています。
>
> Partial Public NotInheritable Class CM
>  Private Sub New()
>  End Sub
> End Class
>
> 機能的には Module でも良いとは思いますが、Module だと名前空間を明示せずにアクセスできてしまうことから、
> 個人的にはあまり好みません。「共通して使うもの」だからこそ、Namespace で明確に管理できた方が、
> 複数のプロジェクトから使う際にも、複数の共通ライブラリを併用しやすいと思っているもので。
>
>>ちなみにCMクラス中にはフィールド変数(要はCMクラスの中で各プロシージャの外で宣言されている変数ですよね?)はありませんが、
>>SolverMain中で非常に高頻度で使う物理定数はCMの先頭においてPublic Constでフィールド定数(?)として宣言しています。
>>これは関係あったりするでしょうか?(作業中に変化しないので無関係?)
> Constを使うことは問題ありません。名前付きリテラル扱いであり、値が変化することは無いですから。
>
> 一方、ReadOnly Field や ReadOnly Property だと、値を変更できる可能性が一応あります。
>
> Class Compiler
>  Shared ReadOnly Months() As Integer = {31,28,31,30,31,30,31,31,30,31,30,31}
>  Shared Function Main() As Integer
>   Console.WriteLine(Months(1))
>   Months(1) = 29
>   Console.WriteLine(Months(1))
>   Return 0
>  End Function
> End Class

ありがとうございます。
CMクラスはあくまで「各フォームで共通して使用するFunctionプロシージャや構造体、定数を定義するための置き場所」であり、CMクラスを
インスタンス化してオブジェクトを作る、ということはしないので、とりあえずはこのままにしようかと思います。


>>またSoverMain中のメソッドがスレッドセーフであることの確認ですが、これは私がCMクラス内に定義したSolverMainやその中で使用される
>>ユーザー定義関数だけではなく、それらの関数中で使用されている演算も含めて、という理解でよろしいでしょうか?
> 「他のスレッドから同時にアクセスされる変数が無い」のならばスレッドセーフです。
> しかし「複数のスレッドで共有されている変数」があるのなら、演算部も含めて
> それらがマルチスレッド対応であるかどうかを、常に意識してコーディングせねばなりません。
>
> たとえば、「64bit 整数型(Long あるいは ULong)を読み取る」というだけの単純処理においても、
> 32bit CPU 環境では、上位32bitと下位32bitの読み取りが命令が発生するため、
> 「読み取り開始から読み取り完了までの間に、他のスレッドによって編集されていた」という可能性が起こりえるため、
> 読み取り処理を不可分とするために、わざわざ Interlocked.Read メソッドという専用命令が利用されています。
> もちろん他のスレッドから同時にアクセスされることのない変数に対しては不要ですけれどね。
>
>
>>(たとえばA = A + B のような通常の加算処理など)
> 変数 A と変数 B の両方がローカル変数な Integer 型であるのならば問題もありません。
>
> しかし、それらの変数のいずれかを ByRef 引数のメソッドに渡していた場合には、
> スレッドセーフではなくなる可能性がありえます。
> (そのメソッド内で別スレッドが生成されており、引数の内容を書き換えてしまう場合など)
>
>
>>CMクラス、またはモジュール中に記述されたSolverMainその他のFunctionプロシージャがプロシージャ内で宣言されたローカル変数か
>>引数としてプロシージャに渡された変数しか使っていない場合は、SolverMainが複数のスレッドから呼び出されても問題はないのでしょうか?
>
> SolverMain がローカル変数しか使っていなかったとしても、その中で「他のメソッド」を呼び出しているなら、
> それぞれのメソッドがスレッドセーフであるかどうかを確認する必要があるでしょう。
>
> 引数として渡された値も、たとえば
>  BackgroundWorker1.RunWorkerAsync(arg(0))
>  BackgroundWorker2.RunWorkerAsync(arg(1))
> みたいな処理をしている場合、それぞれの処理が終わる前に、
> 呼び出し元から arg(0) や arg(1) を読み書きしてはいけないのは言わずもがな。
>
> そういえば現状の実装では、「すべてのスレッドの処理が完了したのかどうか」を
> どうやって判断しているのでしょうか。

ありがとうございます。
SolverMain及びSolverMainから呼び出される他の物理演算用Functionプロシージャのいずれも、各プロシージャ内で宣言されたローカル変数か
Functionプロシージャの引数として渡された物しか使っていないと思います(開発しているのが職場のPCのためすぐに確認できないため)
呼び出し元からargを読み書きしてはいけないというのはつまり
  Dim arg As parameter
  BackgroundWorker1.RunWorkerAsync(arg)

Private Sub BackgroundWorker1_DoWork
  Dim parameter As parameter
  parameter = DirectCast(e.Argument, parameter)

  〜〜
End Sub

のような実装になっている場合に、BackgroundWorker1_DoWorkが実行中にargにアクセスした場合、BackgroundWorker1_DoWork内の
ローカル変数parameterの値も変わってしまい、それによって実行結果が影響を受けてしまうからという理解でよろしいでしょうか?
一応ワーカースレッドが走っている最中は元のargは読み書きしないようにはしています。
SolverMainから呼び出される他の物理演算用のFunctionプロシージャについては値渡しで引数を渡していますが、その場合はどうでしょうか?

なお、全てのスレッドの処理の完了の判断は、PhysicalEngine_RunWorkerCompleted内で

  flagEnd = True
  For i As Integer = 0 To bg.GetUpperBound(0)
    If (bg(i).isBusy = True) Then
      flagEnd = False
      Exit For
    End If
  Next
  If (flagEnd = True) Then
    '結果集計処理
  End If

のようにして全てのスレッドがビジーかどうかをチェックし、ビジーなスレッドがなくなれば全てのスレッドの処理が完了したと判断して結果を集計します。


>>とするとどのように確認をすればいいかご教示いただければと思います。
> 値が「どこで書き込まれているか」「どこで読み取っているか」をきちんと把握した上で、
> それらが「それらは複数のスレッドで同時にアクセスされうるのかどうか」を考えてコーディングします。
>
> そのうえで、「そもそも同時に読み書きさせない」設計にするか、あるいは
> 「並列で読み書きできるよう排他制御を組み込む」ようにする、ということです。

ちょっと混乱してしまったのですが、
・SolverMain、及びSolverMainから呼び出されるFunctionプロシージャはその内部では全てローカル変数か自身の引数しか使用していない
・ワーカースレッドが動作している間、メインスレッド側ではワーカースレッドに渡したパラメータの元の変数にはアクセスしていない
という実装であっても、同時に複数のスレッドからアクセスはされうるということでしょうか?
各フォームのBackgroundWorker_Dowork毎に全く同じSolverMain内部の処理を記述すれば問題は解決するのかもしれませんが、そうなると今度は
物理演算を変更する場合全てのDoWorkを変更する必要が出てきてメンテナンス性が極度に悪化するため避けたいところです。

今ふと思ったのですが、CMクラス内の各Functionプロシージャの全てのローカル変数と、メインスレッドでBackgroundWorkerに渡すパラメータ変数を
スレッドローカル変数にした場合問題は解決しないでしょうか?
スレッドローカル変数自体は使ったことがなく、そもそもそのような実装が可能なのか、何かデメリットはないのか等分からないいので想像ですが・・・
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103554 ] / ▼[ 103559 ]
■103557 / 5階層)  Re[5]: 非同期&並列処理の実装方法について
□投稿者/ 魔界の仮面弁士 (3828回)-(2025/02/16(Sun) 12:36:12)
No103554 (ゆーすけ さん) に返信
> Dim arg As parameter
> BackgroundWorker1.RunWorkerAsync(arg)
> のような実装になっている場合に、BackgroundWorker1_DoWorkが実行中にargにアクセスした場合、BackgroundWorker1_DoWork内の
> ローカル変数parameterの値も変わってしまい、それによって実行結果が影響を受けてしまうからという理解でよろしいでしょうか?

先に例示した
>>  BackgroundWorker1.RunWorkerAsync(arg(0))
>>  BackgroundWorker2.RunWorkerAsync(arg(1))
>> みたいな処理をしている場合、
のような場合、実行中に arg = New parameter() {Nothing, Nothing} などとしても、
最初に渡した arg(0) や arg(1) の値が Nothing に変わるわけではありませんし。

特に、渡した値が「immutable な型」であるならば、e.Argument の内容も変化しないため、
BackgroundWorker 側が影響を受けることもありません。

「immutable な型」とは、Integer 型や String 型、System.Numerics.Complex 構造体などのように、
再代入以外で値が変更されることのない「不変型」のことです。
このような型では、値が決まるのはインスタンス生成時のみです。

ゆえに UserParameter も immutable であることが望ましいわけですが、現実的には必須実行というわけではありません。
殆どの場合、処理中に意図的にパラメータを変えることが無いでしょうから、それを保証できる場合は
明示的に ReadOnly 化することなく、編集可能な型を渡してしまっても動作上は問題ありません。

ただし parameter が immutable な型ではない場合……たとえば Public Points As List(Of ) を
メンバーに含むような型であった場合には、parameter が Class であれ Structure であれ、
 arg(0).Points.Add(p1)
 arg(0).Points.Add(p2)
 arg(0).Points.Add(p3)
などが行われた段階で、実行中の BackgroundWorker1 の e.Argument.Points も動的に変化することになります。

あるいはそうした操作が呼び出し側で行われずとも、BackgroundWorker1 内から呼び出している処理の中で
他のスレッドのパラメーターへの参照がどこかに残っていれば、それを通じて影響を与える可能性はあります。
今回の実装にそのようなコードが含まれているとは思わないですが。


> SolverMainから呼び出される他の物理演算用のFunctionプロシージャについては値渡しで引数を渡していますが、その場合はどうでしょうか?
値渡しであっても、「渡した値が参照型である場合」や「参照型のメンバー(配列など)を含む型」である場合、
その参照を通じて値が変更される可能性があります。

とはいえ、その変更が他のスレッドに影響を与えるものでない場合は問題ありません。


> なお、全てのスレッドの処理の完了の判断は、PhysicalEngine_RunWorkerCompleted内で
>   flagEnd = True
この flagEnd はフィールド変数ではなく、
イベントプロシージャー内のローカル変数(あるいは Static 変数)なのですよね。

For ループを使わずとも、
  If bg.Any(Function(b) b.IsBusy) Then
    Return
  End If
  '以下、結果集計処理
のようにすれば単純化できそうな処理ですが、それはさておき。


残念ながら現状の完了判定が有効なのは、ワーカースレッドが 1 つの場合だけであり、
複数スレッドの終了判定に耐えられるコードにはなっていません。

>   For i As Integer = 0 To bg.GetUpperBound(0)
>     If (bg(i).isBusy = True) Then

ここでいう配列 bg は、最初の質問 No103545 で使われていたローカル変数なのですよね。

たとえば複数のスレッドを順次 RunWorkerAsync している最中に、
先に起動したスレッド(たとえば i = 0)が処理を完了してしまった場合を考えてみてください。

i = 0 が完了した時点で、bg(bg.Length - 1) がまだ Nothing のままであった場合、
bg(i).IsBusy への If 判定が、NullReferenceException に陥る可能性があるかもしれません。

であれば Nothing にならぬよう、起動部分を
  '準備開始
  For i = 0 To numThread - 1
    bg(i) = New BackgroundWorker()
      :
  Next
  'すべて準備できてから一斉に開始
  For i = 0 To numThread - 1
   bg(i).RunWorkerAsync(simParam(i))
  Next
のように書き換えれば良いか…というと、必ずしもそうとも言えず。

この方法だと、終了時の IsBusy 判定が NullReferenceException になることはありませんが、
IsBusy = False という状態が「処理完了済み」なのか「処理開始前」なのか分からないため、
『すべての計算が終わっていない段階』で「結果集計処理」が実行されてしまう恐れがあります。


そこで改善案として、終了のたびに配列の IsBusy を調べる方法はやめて、
RunWorkerCompleted の発生回数を Integer 変数でカウントアップする方法にするのは如何でしょうか。

それを「起動予定のスレッド数であるnumThread」と比較することで、
「完了済みのスレッド数」と「未完了(起動前を含む)のスレッド数」が簡単に分かりますよね。


それからもう一つ。
RunWorkerCompleted の段階では、e.Error が Nothing であるかどうかを確認しておくべきです。

DoWork 中のスレッドがエラー中断した場合、その例外は RunWorkerCompleted の e.Error に伝わりますので、
・例外を記録するなどして原因を調査し、可能であれば例外が発生しないように改める
・エラーが発生したスレッドだけは失敗例として除外し、他のスレッドに対しては計算処理を継続させる
・エラー発生段階ですべて中断させるため、未起動スレッドのRunWorkerAsyncをやめる
・エラー発生段階ですべて中断させるため、起動済みスレッドに対して CancelAsync で中断を要請する
などといった、何らかの追加措置が必要となるでしょう。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103557 ] / 返信無し
■103559 / 6階層)  Re[6]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (7回)-(2025/02/16(Sun) 15:35:31)
2025/02/16(Sun) 17:30:13 編集(投稿者)

No103557 (魔界の仮面弁士 さん) に返信

> のような場合、実行中に arg = New parameter() {Nothing, Nothing} などとしても、
> 最初に渡した arg(0) や arg(1) の値が Nothing に変わるわけではありませんし。
>
> 特に、渡した値が「immutable な型」であるならば、e.Argument の内容も変化しないため、
> BackgroundWorker 側が影響を受けることもありません。
>
> 「immutable な型」とは、Integer 型や String 型、System.Numerics.Complex 構造体などのように、
> 再代入以外で値が変更されることのない「不変型」のことです。
> このような型では、値が決まるのはインスタンス生成時のみです。
>
> ゆえに UserParameter も immutable であることが望ましいわけですが、現実的には必須実行というわけではありません。
> 殆どの場合、処理中に意図的にパラメータを変えることが無いでしょうから、それを保証できる場合は
> 明示的に ReadOnly 化することなく、編集可能な型を渡してしまっても動作上は問題ありません。
>
> ただし parameter が immutable な型ではない場合……たとえば Public Points As List(Of ) を
> メンバーに含むような型であった場合には、parameter が Class であれ Structure であれ、
>  arg(0).Points.Add(p1)
>  arg(0).Points.Add(p2)
>  arg(0).Points.Add(p3)
> などが行われた段階で、実行中の BackgroundWorker1 の e.Argument.Points も動的に変化することになります。

ありがとうございます。
SolverMainに渡す引数はメンバ変数にList等も含んでいますが、ワーカースレッド内でそのListを読み出すことはあっても
変更することはないはずなので、そういう意味ではこの実装でも問題はなさそうですね。


> 値渡しであっても、「渡した値が参照型である場合」や「参照型のメンバー(配列など)を含む型」である場合、
> その参照を通じて値が変更される可能性があります。
>
> とはいえ、その変更が他のスレッドに影響を与えるものでない場合は問題ありません。

記述が足りなくて申し訳ありません、「値渡しで値型の変数を引数として渡している」ということでした。
参照型ではないのでSolverMain以外のFunctionプロシージャについては大丈夫そうです。


> 残念ながら現状の完了判定が有効なのは、ワーカースレッドが 1 つの場合だけであり、
> 複数スレッドの終了判定に耐えられるコードにはなっていません。
>
>>  For i As Integer = 0 To bg.GetUpperBound(0)
>>    If (bg(i).isBusy = True) Then
>
> ここでいう配列 bg は、最初の質問 No103545 で使われていたローカル変数なのですよね。
>
> たとえば複数のスレッドを順次 RunWorkerAsync している最中に、
> 先に起動したスレッド(たとえば i = 0)が処理を完了してしまった場合を考えてみてください。
>
> i = 0 が完了した時点で、bg(bg.Length - 1) がまだ Nothing のままであった場合、
> bg(i).IsBusy への If 判定が、NullReferenceException に陥る可能性があるかもしれません。
>
> であれば Nothing にならぬよう、起動部分を
>   '準備開始
>   For i = 0 To numThread - 1
>     bg(i) = New BackgroundWorker()
>       :
>   Next
>   'すべて準備できてから一斉に開始
>   For i = 0 To numThread - 1
>    bg(i).RunWorkerAsync(simParam(i))
>   Next
> のように書き換えれば良いか…というと、必ずしもそうとも言えず。
>
> この方法だと、終了時の IsBusy 判定が NullReferenceException になることはありませんが、
> IsBusy = False という状態が「処理完了済み」なのか「処理開始前」なのか分からないため、
> 『すべての計算が終わっていない段階』で「結果集計処理」が実行されてしまう恐れがあります。
>
>
> そこで改善案として、終了のたびに配列の IsBusy を調べる方法はやめて、
> RunWorkerCompleted の発生回数を Integer 変数でカウントアップする方法にするのは如何でしょうか。
>
> それを「起動予定のスレッド数であるnumThread」と比較することで、
> 「完了済みのスレッド数」と「未完了(起動前を含む)のスレッド数」が簡単に分かりますよね。
>
>
> それからもう一つ。
> RunWorkerCompleted の段階では、e.Error が Nothing であるかどうかを確認しておくべきです。
>
> DoWork 中のスレッドがエラー中断した場合、その例外は RunWorkerCompleted の e.Error に伝わりますので、
> ・例外を記録するなどして原因を調査し、可能であれば例外が発生しないように改める
> ・エラーが発生したスレッドだけは失敗例として除外し、他のスレッドに対しては計算処理を継続させる
> ・エラー発生段階ですべて中断させるため、未起動スレッドのRunWorkerAsyncをやめる
> ・エラー発生段階ですべて中断させるため、起動済みスレッドに対して CancelAsync で中断を要請する
> などといった、何らかの追加措置が必要となるでしょう。


情報が不足していました。
このflagEndはワーカースレッドを呼び出すフォームで使っているフィールド変数でstatic属性はついていません。
基本的に各ワーカースレッドの起動はフォームがロードされた時に自動で一度だけ行われ、また各ワーカースレッドの処理には
それなりの長時間(数分かそれ以上)がかかるので、順次RunWorkerAsyncしている最中に先に起動しておいたワーカースレッドの
処理が完了することはありません。
ただ、万一エラーでワーカースレッドが落ちた場合のことを考えた実装は確かにしていませんでしたので、教えていただいた点は
今後の開発時には留意して実装しようと思います。


> Function プロシージャ―は、「そのメソッドを呼び出したスレッド」の中で実行されますので、
> CM 側で新しいスレッドを作られるというのでなければ、ローカル変数だけを渡すようにしておけば問題ないでしょう。
>
>
> 心配ならば、SolverMain を共有メソッドからインスタンスメソッドに変更し、
> DoWork 内で、Dim c As CM なローカル変数を持たせて、各スレッドごとに CM を New すると言う手もあります。
>
> こうすれば、たとえ CM クラスに「Private フィールド」を持たせたたとしても
> それぞれのスレッドで独立した値となりますので、他のスレッドに影響を与えることはありません。
>
> 受け渡す処理パラメーターについては、「Dim c As New CM(e.Argument)」のようにして、
> CM クラスの Public コンストラクタで渡すようにしても良いですし、フィールドが不要なのであれば、
> 現状のまま、SolverMain の引数と戻り値だけで済ませることもできます。
>
> とはいえ、現状 Public Shared Function SolverMain だけで完結しており、
> CM 側ではフィールド変数が使われていないという話なので、
> 現時点ではインスタンスメソッドへの改修は必要ないと思います。
>
>
> いずれにせよ、どの段階で意図しない値に変化しているのかも分からないままで
> 無暗にソースに手を加えることはお奨めしません。

CMクラス内で新たにスレッドを生成することはないので問題はなく、スレッドローカル変数を使う必要もないということですね。
ちなみに、改修ではなく新規にフルスクラッチで開発する場合だとどうでしょうか?
実のところ、現在行っている物理シミュレーションの内容をより高度にするため、プログラムを完全に一から作り直す必要に迫られています。
ですのでその際にもしスレッドローカル変数を使うことで問題が解決できるのであれば、選択肢の1つとして考えておきたいと思っています。
上記のDoWork内でCMクラスのインスタンスを生成して各ワーカースレッド毎に別々のインスタンスで物理計算を行わせるのもよさそうですが、
それにはインスタンスメソッドの使い方を調べる必要がありそうです。


> たとえば個々のスレッド処理には問題が無いものの、
> 複数の結果を集約させる段階で、何らかのミスがあったのかもしれません。
>
> Debug.WriteLine などで、各 idx ごとの計算結果を ThreadId と共にログとして出力させてみたり、
> あるいは複数スレッドを処理させている途中で一時停止させてみて、
>  [デバッグ]>[ウィンドウ]>[並列スタック]
>  [デバッグ]>[ウィンドウ]>[スレッド]
> などを覗いてみて、変数値が不自然になっていないかを確認するなど…。

確かにその可能性もありますね。
実はCM.SolverMainに与える初期条件simParamのメンバ変数のうち、一部のパラメータを特定の値にしておくとマルチスレッド時に値が狂う事象は
発生せず、シングルスレッド時と同じ計算結果になることが分かっています。
この結果と、今回いろいろご教示いただいた点を考えあわせますと、実はCMクラスのSolerMainや他のFunctionプロシージャの実装自体には問題はなく
どこか別のところに原因があるのかもしれません。
最初に挙げたコードでは本筋とは関係ないかもと思い省略してしまいましたが、DoWork内部には

Private Sub PhysicalEngine_DoWork(sender As Object, e As Syste.ComponentModel.DoWorkEventArgs)
  Dim InternalParam As CM.UserParameter
  Dim InternalResult As CM.SimulationResult

  InternalParam = DirectCast(e.Argument, CM.UserParameter)

  Do
    InternalParamの一部変更処理
    InternalResult = CM.SoverMain(InternalParam)
    〜〜
    If 終了条件 Then
      Exit Do
    End If
  Loop
  e.Result = InternalResult
End Sub

のように、初期パラメータInternalParamの一部をその都度変更して繰り返しSolverMainを呼び出している箇所があります。
このInternalParamの設定・一部変更部分の実装に問題があり、あるワーカースレッドで初期パラメータ設定や一部変更を行った結果、それが他のワーカースレッドにも
影響を及ぼしているのかもしれないという気がしてきました。
週明けに出勤したら並列スタックや並列ウォッチを使って各ワーカースレッドにおける変数値を一度確認してみようと思います。
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103554 ] / 返信無し
■103558 / 5階層)  Re[5]: 非同期&並列処理の実装方法について
□投稿者/ 魔界の仮面弁士 (3829回)-(2025/02/16(Sun) 13:59:41)
No103554 (ゆーすけ さん) に返信
> CMクラス内の各Functionプロシージャの全てのローカル変数と、メインスレッドでBackgroundWorkerに渡すパラメータ変数を
> スレッドローカル変数にした場合問題は解決しないでしょうか?
> スレッドローカル変数自体は使ったことがなく、そもそもそのような実装が可能なのか、何かデメリットはないのか等分からないいので想像ですが・・・

Function プロシージャ―は、「そのメソッドを呼び出したスレッド」の中で実行されますので、
CM 側で新しいスレッドを作られるというのでなければ、ローカル変数だけを渡すようにしておけば問題ないでしょう。


心配ならば、SolverMain を共有メソッドからインスタンスメソッドに変更し、
DoWork 内で、Dim c As CM なローカル変数を持たせて、各スレッドごとに CM を New すると言う手もあります。

こうすれば、たとえ CM クラスに「Private フィールド」を持たせたたとしても
それぞれのスレッドで独立した値となりますので、他のスレッドに影響を与えることはありません。

受け渡す処理パラメーターについては、「Dim c As New CM(e.Argument)」のようにして、
CM クラスの Public コンストラクタで渡すようにしても良いですし、フィールドが不要なのであれば、
現状のまま、SolverMain の引数と戻り値だけで済ませることもできます。

とはいえ、現状 Public Shared Function SolverMain だけで完結しており、
CM 側ではフィールド変数が使われていないという話なので、
現時点ではインスタンスメソッドへの改修は必要ないと思います。


いずれにせよ、どの段階で意図しない値に変化しているのかも分からないままで
無暗にソースに手を加えることはお奨めしません。
掲示板に投稿されていない範囲で、明確に NG となる実装(フィールドの共有など)が
あるというのであれば別ですが、問題個所がまだ特定できていないなら、
まずはデバッグを繰り返して、計算結果が変わってしまう原因を捉えることを優先させましょう。
(むしろ障害の発生頻度が高い方が、調査しやすい状況だとも言えます)

たとえば個々のスレッド処理には問題が無いものの、
複数の結果を集約させる段階で、何らかのミスがあったのかもしれません。

Debug.WriteLine などで、各 idx ごとの計算結果を ThreadId と共にログとして出力させてみたり、
あるいは複数スレッドを処理させている途中で一時停止させてみて、
 [デバッグ]>[ウィンドウ]>[並列スタック]
 [デバッグ]>[ウィンドウ]>[スレッド]
などを覗いてみて、変数値が不自然になっていないかを確認するなど…。

下記に、マルチスレッド アプリケーションのデバッグ方法や、複数のスレッドをデバッグするためのチュートリアルが掲載されています。
https://learn.microsoft.com/ja-jp/visualstudio/debugger/debug-multithreaded-applications-in-visual-studio?WT.mc_id=DT-MVP-8907&view=vs-2022
[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103545 ] / ▼[ 103556 ]
■103555 / 1階層)  Re[1]: 非同期&並列処理の実装方法について
□投稿者/ WebSurfer (2939回)-(2025/02/16(Sun) 10:51:55)
No103545 (ゆーすけ さん) に返信

話が進んでいっているところで今さらこういうことを聞くのもなんですが・・・

マルチスレッドアプリで処置時間の短縮を図るならマルチコアを利用できる環境が
必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になる
と思いますが、そのあたりは考慮されているのでしょうか?

以下の記事の画像の A のようになっていると、OS がスレッドを切り替えて処理
を行うのでそのオーバーヘッドの分逆に遅くなるはずです。

タスク並列ライブラリ (TPL)
https://surferonwww.info/blogengine/post/2020/12/27/task-parallel-library.aspx

[ 親 103545 / □ Tree ] 返信 編集キー/

▲[ 103555 ] / 返信無し
■103556 / 2階層)  Re[2]: 非同期&並列処理の実装方法について
□投稿者/ ゆーすけ (6回)-(2025/02/16(Sun) 11:09:36)
No103555 (WebSurfer さん) に返信
> ■No103545 (ゆーすけ さん) に返信
>
> 話が進んでいっているところで今さらこういうことを聞くのもなんですが・・・
>
> マルチスレッドアプリで処置時間の短縮を図るならマルチコアを利用できる環境が
> 必要で、さらにマルチコアを有効に利用するプログラミングを行うという話になる
> と思いますが、そのあたりは考慮されているのでしょうか?
>
> 以下の記事の画像の A のようになっていると、OS がスレッドを切り替えて処理
> を行うのでそのオーバーヘッドの分逆に遅くなるはずです。
>
> タスク並列ライブラリ (TPL)
> https://surferonwww.info/blogengine/post/2020/12/27/task-parallel-library.aspx
>

ありがとうございます。
一応その辺りは考慮済みです。
元々ワーカースレッドの数はPCに搭載されている論理CPUコアの数以下しか指定できないようにしていますし、
個々のワーカースレッドは完全に独立していて処理完了後にメインスレッドに結果を渡して終了するだけであり、
ワーカースレッド同士で何かデータをやり取りしたり同期をとったりといったことはありません。
実際、開発したプログラムは上記の問題(計算する度に結果が狂う)を別にすれば高速化自体は問題ありませんでした。
[ 親 103545 / □ Tree ] 返信 編集キー/


管理者用

- Child Tree -