質問: 複数機器を制御するアプリケーションのマルチスレッド設計について いつもお世話になります。 主題の件について、長文になってしまいますが、ご意見・ご教授をお願い致します。 前提条件 ----- ##### システム要件 例として、以下の図のようなイメージ構成のシステムを考えます。 今回作成するアプリケーションは`ソフトウェアFOO`に該当します。 ![イメージ説明](https://teratail.storage.googleapis.com/uploads/contributed_images/cd9995b3248290d2b3b06d6924b735fd.png) このシステムは、以下の動作を行います。 * `FOO`は、複数のマシンを制御するアプリケーションである * 各マシンは非同期的に動作を行う * `FOO`は各マシンと通信にて制御を行う * `FOO`は基本的にはサーバ的な役割を行う(マシンから通知が来たら、何かしらの処理をして返す) * ただし、`FOO`から能動的に、各マシンへ動作を指示する場合もある (例えば、停電発生時に各機器への縮退指示など) * 図にはないですが、`FOO`内の各コントローラは、例えば「電源状態」とか「システムスケジュール」といった共通のステータスを参照する * 図にはないですが、`マシンA`が動いているときは、`マシンB`は停止するといった、マシン間の制御も行う必要がある ##### 開発環境 * C#を使用した.NETアプリケーション ##### 現状のソフトウェア設計 * 各コントローラは1つのコントローラクラスのインスタンスとして、Factoryクラスから生成する 悩んでいる点 ------- ここで、「マシンAのコントローラ」等の、各コントローラでどのように処理を実現すべきか悩んでいます。 具体的に、各コントローラはそれぞれ非同期(ときには同期)で処理を行うことになるのですが、以下の2パターンを構想しています。 <構想1> * コントローラクラスを常駐スレッド化する →つまり、各コントローラそれぞれに常駐スレッドを設ける * インフラストラクチャー層とはスレッド間メッセージキュー等でやり取りして、各マシンの制御を行う <構想2> * コントローラクラスは常駐スレッドを持たない * マシンからメッセージを受け取ると、インフラストラクチャー層からコールバックでコントローラへ通知を行い、それをきっかけとして非同期処理を開始し、処理が終わると非同期処理を終了する。 (イメージとしては、C#でいうと、`Task.Run()`で非同期処理を行い、各コントローラごとの常駐スレッドはなしで処理を行う) つまり、構想1・2において、「常駐スレッドをたてる」か「通知が来たときのみ非同期処理を行う」かの違いがあります。 それぞれ、以下の特徴があると考えています。 * 構想1は、コントローラインスタンスというオブジェクトが、メッセージを受け取ると能動的に処理を行うという点において、オブジェクト指向っぽいやり方である。 ただし、複数スレッドが常駐するため、全体としての制御が難しい? * 構想2は、必要な時に必要なタイミングでマルチスレッドで動作を行うので、スレッドの資源を有効的に使える。 そして、C#ではマルチスレッド処理は`Task`を使用するのが一般的と聞いたので、C#で実現するならこちらのほうが推奨されている? #### <補足>ソフトウェアに要求される性能 ソフトウェア的には、パフォーマンスはそこまで要求されません。 どちらかというとメンテナンス性(変更容易性や可読性など)が要求されます。 質問点 ---- * 上記の構想1・2において、どちらを選択すべきでしょうか。(または他に選択すべき方法があるでしょうか) →どちらがメンテナンス性に優れているでしょうか。 * どちらのやり方が一般的なのでしょうか。 個人的な考えとしては、C#の時流が`Task`による非同期処理なので、構想2が妥当なのかなと思いますが、メンテナンス性や可読性は構想1の方が優れているのかなと感じています。 また、以下の点においてもご意見頂ければ助かります。 * そもそも、それぞれの構想の特徴の捉え方は間違っていないでしょうか。 * もし同じような経験をされた方がおられましたら、アドバイス頂けますでしょうか。 以上、長くなりましたが、よろしくお願い致します。
回答: こんにちは。 マシンとの通信が状態を持たない単発の1往復で完了し、次の単発との関連がない場合は「構想2」で全体の構造を単純に保つのは好ましいと思います。 しかし、もし、例えば「マシンからA要求がきたら、a応答を返しそのままB要求をサーバ側から送って...」みたいな複雑な通信処理が必要になるなら、その「シーケンス」1つ毎に「スレッド」を割り当てて同期処理した方が見通しはよくなります。 その「シーケンス」がせいぜい2~3ステップ程度の単純なものなら、async関数で実装し、各ステップをawait Task.Runで実装すると楽できます。await Task.Runの前後でコントロールを触れるからなかなか楽です。(例外がApplication.ThreadExceptionに届かないのでちょっとアレですが。) しかし、async関数はたいへん残念なことに非asyncなメソッドを呼び出してそのメソッドの中でawaitできないのであまり大きな処理を作ることはお勧めしません。(内部で通信するようなメソッド呼び出しを使わないで書いても問題のない程度の処理でないと厳しいです。) 通信制御がそれ以上に複雑になる場合は、構想1のように常駐スレッドを起こすのが良いだろうと感じます。 > > 複数スレッドが常駐するため、全体としての制御が難しい? > > > 常駐スレッドの場合、そのスレッドを終了させる機能が必要になります。きちんと適切に設計できれば簡単なのですが、意外にはまります。マイクロソフトは同期式I/Oに中断機能を設けないことが多いからです。中断させるためだけに非同期I/Oにしないといけないことも度々あります。 VistaでやっとまともにサポートされたI/Oのキャンセル機能 長時間稼働しないスレッドしかないなら、キャンセル処理しないのも選択肢に入るので手間を減らせます。アプリ終了時もちょっと待っている内に終了するのですから。 あとTaskを使う場合は複数のTaskを一度に開始しようとすると遅延が発生するを見ておくと良いかも知れません。 私はこれにはまりました。スレッド起動に数秒かかるとか普通は有り得ないので意外にはまります。