プログラミング応用b 第11回 『スレッド2』

【「待機」と「通知」によるスレッド協調】

 行いたい処理によっては,スレッドを協調させて動作させる必要が出てくる。たとえば,
スレッド A がリソースを生産し,スレッド B がそのリソースを消費するような場合である。
このような関係を,一般に「プロデューサー(生産者)/コンシューマー(消費者)」関係と言う。

例えば,
 ・通信スレッド(生産者)がデータを受信し,そのデータを描画スレッド(消費者)が受け取って描写する。
 ・在庫管理スレッド(生産者)が入荷処理を行い,販売出荷スレッド(消費者)が在庫品を発送する。
 ・協力型ゲームソフトで,参加者の待合室スレッド(生産者)が協力プレイ希望プレイヤーの受付及び登録処理を行い,
   マッチングスレッド(消費者)がプレイヤーどうしを組み合わせて協力プレイに送り出す。
といった例が考えられる。

 このようなケースで重要なのは,
・生産者スレッドがリソースを生産するまで,消費者スレッドを「待機」させ,
・生産が終わったら,待機していた消費者スレッドにその旨を「通知」して再開させる
という仕組みである。java では, Object クラスに用意されている wait( )notify( )notifyAll( )
というメソッドで,スレッドの「待機」と「通知」の仕組みを提供している(Table 2)。


 「待機」と「通知」の基本的な仕組みは,次の通りである。

(1)スレッドの同期コードがロック用オブジェクトの wait( ) メソッドを呼ぶと,ロックを解放して,そのスレッドは待機状態に入る。
  ※つまり,行儀良く自主的に待機するために wait( ) を呼び出す。
(2)wait( ) によって待機状態に入ったスレッドを再開させるには,ロック用オブジェクトの notify( ) メソッドか notifyAll( ) メソッドによって「通知」を行う
(3)notify( ) メソッドは,そのロック用オブジェクトの wait( ) メソッドによって待機状態になっているメソッドのうち,どれかひとつだけに「通知」を行う。
(4)nofity( ) を呼び出したスレッド(の同期コード)自体は,他のスレッドに動作のチャンスを与えるためにロックを解放する。
   その後,「通知」を受けた待機中のスレッドは,ロックが与えられて実行が再開される。

  ※notify( ) メソッドは,「通知」するスレッドを指定できない。よって,待機しているスレッドがひとつだけであるときに使用すべきである。
(5)notifyAll( ) メソッドは,そのロック用オブジェクトの wait( ) メソッドによって待機状態になっているすべてのスレッドに「通知」を行う
(6)nofityAll( ) を呼び出したスレッド(の同期コード)自体はロックを解放した後,待機状態になる。かわりに待機中の全スレッドのうち「通知」されたひとつが,そのロックを与えられて,実行が再開される。
   ロックを与えられなかった他のスレッドは引き続きロックの解放を待ったままとなる。


 実際の使用例を List 12, List 13 に示す。これは,ケーキ屋のシミュレーション(List 4, List 6)
を変更したもので,ケーキ屋の店頭でケーキが売り切れた場合は,ケーキを焼いて追加する
という処理を加えている。客は,「待機」と「通知」の仕組みを使って,ケーキが売り切れた場合は,
ケーキが追加されるまで待って,ケーキが追加されたらケーキを買う,という行動を取るようにする。

List 12 ShopSim.java, List 13 CakeShop.java



 では,実際にプログラムを見てみよう。List 12 が List 4 と違うのは, List 12-① で,
CakeShop クラスに新たに導入した addANewCake( ) を呼んで,5個のケーキを追加
しているところだけである。
 主な「通知」と「待機」の仕組みは, List 13 の Cakeshop クラスの定義に仕掛けられている。
List 13-①があらたに加えられた同期メソッド addANewCake( ) の定義で,ケーキを
加えた後, notifyAll( ) を呼んで「通知」を行っている。
ケーキ屋さんが『ケーキが焼けたよ〜』と,待っているお客さん達に声をかけるイメージである。
ただし,それに気づく客は一人のみであることに注意。

  List 13-②では,

    while( getNumOfCake( ) <=  0 ) {
      try{ wait( ); }
      catch( InterruptedException ie){ }
    }

という形で,ケーキが1個以上になるまで「待機」を行っている。
 一般に,「待機」は,
    while( /*待機条件*/ )  {
      try{ wait( ); }
      catch( InterruptedException ie ){ }
    }
    //待機後の処理

というループの形で行うべきである。なぜなら,同じロック用オブジェクトの wait( ) で
待機状態に入っているスレッドでも,待機の理由自体はスレッドごとに異なっている可能性
があるためである。つまり,「通知」されて再開したものの,実はそのスレッドの待機条件は
まだ真のままであることがあるわけだ。ケーキ屋の例で言えば,ケーキ屋の前で待っている
客は,新しいケーキが追加されるのを待っているとはかぎらず,クッキーが焼き上がるのを
待っているのかもしれない,ということである。

 それだけでなく,「通知」を受け取った後から,待機条件が真になってしまうこともあり得る。
たとえば,List 13 の例では,ケーキが追加されてスレッドに「通知」された後で,待機していた
スレッド T1 がケーキを買ってしまい,ふたたび0個になったところで,別の待機していた
スレッド T2 が再開されるかもしれない。

 このことから,待機が解除されたら,まだ待機しつづけるべきか条件判定をし,待機しな
くてもいいときだけ,ループを抜けて,処理を行うようにする必要があると言える。
 List 12 と List 13 を実行すると,客は全員,ケーキを買うことができることがわかる。

 


【まとめ】
 紹介できなかったスレッドに関する話題はまだあるが,以上の基本を押さえればスレッド
を使いこなせるだろう。特に同期による排他処理デッドロック回避に注意すること。
スレッドに関するさらなる機能は,参考書やJava APIドキュメントを呼んで,勉強してみよ。

【次回は】
 次回は,GUIに関する開設をする予定である。