ニートが学ぶ非同期処理 Monitorクラス

はいどーもこんにちは、一日中モニターを見続けているニートです。
本日も引き続き非同期処理について勉強していきますよ。
今回はMonitorクラスです。
参照するのはこの記事。
http://www.atmarkit.co.jp/ait/articles/0506/15/news114_2.html

Monitorクラスってなに

前回の記事でlockステートメントを取り扱いました。
実はこのlockステートメントは、MonitorクラスのEnter/Exitメソッドを使用しているのだと言います。
つまりこのクラスの挙動をちゃんと把握しておけば、lockステートメントの挙動も間違いなく理解できるということになりそうですね。

前回作成したコードを書き換えてみる

lockステートメントと同じように使えるというわけで、まずは前回の記事で作成したコードをMonitorクラスのバージョンに書き換えてみます。
元のコードを一応引用しておきます。

これをMonitorクラスを使ってロックするなら、こうです。

出力する値を100に変えましたが、それ以外は同じです。
もしもMonitorクラスでロックしていなければ、最終的に出力されるNumが95とか94とかのデタラメな数字になっちゃう場合もあります。

Monitorクラスを少しだけ見てみます。
Monitor.Enterの第一引数でオブジェクト(_lockObj)をロックしようとします。
もしもオブジェクトをロックしようとしたときに、すでにオブジェクトがロックされていたならば、そこで処理はいったん停止します。
ロックしている側の処理がfinally句に進み、Exitでオブジェクトを解放すると他のスレッドは止めていた処理を再開します。
それだけですね。

Producer-Consumerパターン

ここまでの話はlockを理解していれば直感的にわかることかなと思います。
参照記事ではこの後、Producer-Consumerパターンというものを紹介しています。
この動きがややこしいので、一つ一つ分解して理解していこうと思います(基本的な説明とコードは元記事を見てください)。

まずテーブルクラスを見ていきます。
テーブルに置くことができる料理の最大数は3です。
テーブルの実態としてはQueueが使われていますが、今ならジェネリック型を使うのが妥当でしょう(元コードではわざわざstring型にキャストしてる箇所がある)。

テーブルクラスには二つのメソッドが定義されています。
すなわちPutとTakeです。
Putは料理人がテーブルに料理を置くことを意味しています。
逆にTakeは客がテーブルから料理を取る(テーブルから取って自分の席に持っていく感じ?)ことを意味しています。

PutもTakeも処理内容としては似ています。
Putはテーブルがいっぱい(3つ以上の料理が置かれている)なら、Waitセットに入ります。
まだ余裕があるなら、テーブルに料理を追加します(キューに追加してるだけ)。
そしてMonitor.PulseAllを実行し、各スレッドに処理を行わせます。

Takeはこれの逆で、キューが空(テーブルに料理がなければ)ならWaitセットに入ります。
料理があるなら料理を取っていきます(誰でも取れるが、このプログラムでは客が取る)。
そのついでにMonitor.PulseAllを実行ですね。

Producerクラスを見ていきます。
これはコンストラクタで先ほど作成したテーブルを受け取っています(依存性の注入)。
いくつかのスレッドで同時に実行されることを前提としているため、Produceメソッド内のid++の部分をロックしています(なおidフィールドはstaticなので全てのインスタンスで共通)。
あとはテーブルに料理をPutして完了です。

Consumerクラスも見ます。
これもProducerクラスと同様にコンストラクタでテーブルを受け取っています(依存性の注入)。
Consumeメソッドは無限ループさせつつ、テーブルからTakeする処理を書いているだけですね。
Consumeメソッドがなぜかpublicになっているのは単なるミスだと思います(privateでも何ら問題ないので)。

残るはMainメソッドです。
まさにここは依存性を注入するための典型的な場所です。
というわけでテーブルクラスのインスタンスを作っていますね。
作ったインスタンスをProducerクラスのインスタンスとConsumerクラスのインスタンスに渡してスレッドを開始させています。

で、実行するとどうなるか。
テーブルが空いていたら料理がテーブルに置かれます。
テーブルに料理が置いてあったら、客は料理をテーブルから取っていきます。
それだけですw

これが一体なんなのかとうことなのですが、以下のサイトの説明がわかりやすかったです。
https://www.hyuki.com/dp/dpinfo.html#ProducerConsumer
非同期処理のキモとしては「テーブルの状態をチェックし始めた瞬間から実際の行動に移すまでの間、他のスレッドに処理を割り込ませない」ことかなと思います。
これがテーブルクラスのロック用オブジェクトでやっていることですね。

いくつものスレッドが同時にPutやTakeを実行しようとしますが、Monitor.Enterで「今はロックされている」ことを伝えますし、状態が変化したらMonitor.PulseAllで他の止めていたスレッド全てを一斉に起こしています。
また、Monitor.Waitによって「一度ロックしていた状態を破棄(ロック解除)」して、他のスレッドがロック内に入れるようにもしています(解除した側の進行は止まる)。

終わりに

ぶっちゃけ、かなりややこしいですね。
僕が実際の開発でこんな方法を取ることはあるんでしょうかね……。
ウーン、バグまみれになりそうだから多分ないような……。

ほなそんな感じでまた。

フォローする