前回の投稿の独断と偏見に満ちたまとめをする
注意
完璧に独断と偏見です。
MultiThreadデザインパターンごとのまとめ
ここに書くのは、基本的には個人的なパターンの使いどころ。
必要であれば補足する。
1. Single Thread Execution パターン
- メソッドを、スレッドセーフなもの/そうでないもので区別するだけ
- スレッドセーフでないものは
lock
させましょうねー
- スレッドセーフでないものは
- 概念であり、デザインパターンとは言えないだろう
2. Immutable パターン
- クラスを、メンバ等の変化がないもの/そうでないもので区別するだけ
- メンバ等の変化ないものは、クラス自体がスレッドセーフといえる
- これも概念であり、デザインパターンとは言えないだろう
3. Guarded Suspension パターン
- スレッド間メッセージキューのパターン
- メッセージがない間は、受け取り側スレッドを待機させる
-
BlockingCollection
を使えば、何も考えなくてもメッセージキューが作れる
補足
- 本当の意味は、受け取り側にガード条件を設けて、ガード条件に合致しない場合は、スレッドを待ちにさせる。
- ガード条件が「メッセージが存在するか」という最もシンプルな場合は、上記のメッセージキューに当てはまる。
- ガード条件が複雑な場合は、
Wait/Pulse
を駆使する必要がある。 - けど、キューは排他機能付きの~~
ConcurrentQueue
~~BlockingCollection
を使うべし。
- ガード条件が複雑な場合は、
4. Balking パターン
-
Guarded Suspension パターン
の亜種 - ガード条件に合致しないときは、処理を中止する
- 中止することで、パフォーマンス向上を図る
-
Guarded Suspension パターン
に、不要だったら処理を中止するという機能を付加するというイメージ
5. Producer-Consumer パターン
-
Guarded Suspension パターン
で、メッセージの送り手と受け手の両方にガード条件が存在するパターン- 例えば、メッセージキューでキューの上限が決まっていて、送り手側でキューが上限に達していたら待つ処理を追加するときに用いる
- つまり、
Guarded Suspension パターン
に機能を付加するイメージ
6. Read-Writer Lock パターン
- 2種類の
lock
を使用して排他制御する- 書き込み用ロック:ロック中は、書き込みも読み込みもできない
- 読み込み用ロック:ロック中は、読み込みはできるが、書き込みはできない
- ロックが不要な処理に対してはロック条件を緩くすることで、パフォーマンス向上を図る
-
ReaderWriterLockSlim
を使用すれば容易に実現できる
7. Thread-Per-Message パターン
- たぶん、受け取り用常駐スレッドが存在しないただの非同期処理
- 非同期処理で実行した結果は特に待たない
- つまり、
Task.Run()
してawait
やWait
しないパターン
- デザインパターンとして考えなくてよいだろう
8. Woker Thread パターン
-
Producer-Consumer
パターンのスレッドプール使用版 - C#ではスレッドは
Task
=スレッドプールを使用するのがデファクトスタンダードなので、Producer-Consumer
パターンと同一視して問題ないと思う
9. Future パターン
- 非同期処理だけど、クライアント側の好きなタイミングで結果を受け取る
- 待ち受ける際、すぐにクライアント側に制御が返る(引換券)
- 結果は後から受け取る
-
async/await
のことと思ってたけど、必ずしもそうではない。- 開始と待ち受けが、異なるメソッドの時は
Task.Start
とTask.Wait
という使用する。
- 開始と待ち受けが、異なるメソッドの時は
- クライアント側はマルチスレッドを意識しない(あたかもシングルスレッドで動作しているかのよう)というメリットもある
補足
一応補足しておくと、Future
パターンはThread-Per-Message
パターンの亜種
10. Two-Phase Termination パターン
- スレッド終わるときに、後始末をしてから終わるようにすること
- 2段階の終了処理
- あたりまえのこと。スレッドのたしなみ
- わざわざデザインパターンという程のものではない
11. Thread-Specific Storage パターン
- 使わない。不要。
- スレッド固有のコインロッカー。だけど、C#では基本的にスレッドプールで処理する(=スレッドを使いまわす)ので、スレッド固有という概念が使えない
- そもそも、使えなくても何の問題もない
12. Active Object パターン
-
Futureパターン
にWokerThreadパターン
を組み合わせたもの - ごちゃごちゃしているけど、結果的に
Future
パターンとの違いは、
「固定されたスレッドがワーカーとして実処理を行う」ということ - つまり、ワーカースレッドはシングルスレッドで処理する必要がある(例えばスレッドセーフにできないとか)ときに使える
- クライアントスレッドへはすぐに引換券(疑似結果)が返ってくる。実際の結果は、クライアントスレッド側の好きなタイミング受け取る。
- 受け取りフラグみたいなのを、ワーカースレッド側でONにして、フラグONでクライアント側は結果格納先にアクセスできるようにする
- クライアントスレッドはマルチスレッドで動作させて、複数クライアントに対応できる
- クライアントスレッドへはすぐに引換券(疑似結果)が返ってくる。実際の結果は、クライアントスレッド側の好きなタイミング受け取る。
まとめのまとめ
以上より、強引にまとめると、覚えておくべきパターンは以下の4通り
- WorkerThreadパターン
- スレッド間での同期処理
- 必要であればBalkingの機能を追加する
- Futureパターン
- 非同期処理をしつつも、クライアント側の好きなタイミングで結果を受け取る
- ActiveObjectパターン
- Futureパターンにしたいが、処理するスレッドが1つに限定される場合にWorkerThreadパターンを融合する
- Read-Writer Lockパターン
- 書き込みと読み込みが存在するときの排他制御
(Two-Phase Terminationパターンは重要だが、当たり前のことなのでカウントしていない)