[C#備忘録]<独断と偏見まとめ>MultiThreadデザインパターンのC#版について


前回の投稿の独断と偏見に満ちたまとめをする

注意

完璧に独断と偏見です。

MultiThreadデザインパターンごとのまとめ

ここに書くのは、基本的には個人的なパターンの使いどころ。
必要であれば補足する。

1. Single Thread Execution パターン

  • メソッドを、スレッドセーフなもの/そうでないもので区別するだけ
    • スレッドセーフでないものはlockさせましょうねー
  • 概念であり、デザインパターンとは言えないだろう

2. Immutable パターン

  • クラスを、メンバ等の変化がないもの/そうでないもので区別するだけ
    • メンバ等の変化ないものは、クラス自体がスレッドセーフといえる
  • これも概念であり、デザインパターンとは言えないだろう

3. Guarded Suspension パターン

  • スレッド間メッセージキューのパターン
    • メッセージがない間は、受け取り側スレッドを待機させる
    • BlockingCollectionを使えば、何も考えなくてもメッセージキューが作れる
補足
  • 本当の意味は、受け取り側にガード条件を設けて、ガード条件に合致しない場合は、スレッドを待ちにさせる。
  • ガード条件が「メッセージが存在するか」という最もシンプルな場合は、上記のメッセージキューに当てはまる。
    • ガード条件が複雑な場合は、Wait/Pulseを駆使する必要がある。
    • けど、キューは排他機能付きのConcurrentQueueBlockingCollectionを使うべし。

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()してawaitWaitしないパターン
  • デザインパターンとして考えなくてよいだろう

8. Woker Thread パターン

  • Producer-Consumerパターンのスレッドプール使用版
  • C#ではスレッドはTask=スレッドプールを使用するのがデファクトスタンダードなので、Producer-Consumerパターンと同一視して問題ないと思う

9. Future パターン

  • 非同期処理だけど、クライアント側の好きなタイミングで結果を受け取る
    • 待ち受ける際、すぐにクライアント側に制御が返る(引換券)
    • 結果は後から受け取る
  • async/awaitのことと思ってたけど、必ずしもそうではない。
    • 開始と待ち受けが、異なるメソッドの時はTask.StartTask.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パターンは重要だが、当たり前のことなのでカウントしていない)