C#
.NET
マルチスレッド
デザインパターン

[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パターンは重要だが、当たり前のことなのでカウントしていない)