| |||
Observer パターン | |||
まず,復習として,Observer パターンの構造と協調関係を図に示します. Java による簡単なコード例も書いて見ました. |
|
|
|
構造での注目点は,抽象クラス Subject と Observer 間の依存関係 と,それらを継承した具体クラスである ConcreteSubject と ConcreteObserver 間 の依存関係が逆転していることです.図1 で,関連の矢印が逆向きになっている 点に注目してください. Java では,Subject(java.util.Observable) と Observer(java.util.Observaber)は JDK コアライブラリの一部として提供されており, これらを継承してアプリケーションで ConcreteSubject と ContreteObserver を作成します. Subject は内部に Observer の列を保有する形で Observer に依存していますが, 一般に ConcreteSubject は ConcreteObserver に依存しません. あるクライアントが ConcreteSubject の状態を変更した際の協調は, 以下のように行なわれます.
協調関係での注目点としては,変更通知の「タイミング」は ConcreteSubject が 握っているが,変更をどう扱うかという「解釈」,は ConcreteObserver 側に 任されているという点です.ConcreteObserver は変更通知を無視することも できますし,さらに第3者に通知することも可能です. MVC(Model View Controller)フレームワークにおいては, ConcreteSubject = Model, ConcreteObserver = View としてこの パターンが利用されます.Model は View についての知識を持たせず, 更新があったタイミングのみを Update() によって View に通知する, という形態を取ります.逆に,View は Model についての知識を持ち, 更新の内容を解釈することを任されています. Observer パターンの協調においては,Subject 側のオブジェクトが タイミングに関しての責任者であり,Observer側のオブジェクトが 変更の解釈に関しての責任者となっています. 具体クラスの ConcreteSubject と ConcreteObserver では, 呼出し関係の方向と依存関係の方向が逆転している,と捉えることもできます. このように,Observer パターンの主題は「逆転」である,と言えます. これまでに挙げた3つの逆転をまとめてみます.
Observer パターンはこの逆転を利用することで,例えば「コールバック」 のような,知らない相手への通知機構の実装に利用される パターンとなっています. |
マルチスレッド環境とデッドロック |
Observer は非常に頻繁に利用されるデザインパターンの花形 とも言ってよいパターンです.特にデザインパターンの入門として このパターンがよく取り上げられること,また,java.util パッケージに 標準で含まれたていることで,最も有名なデザインパターンと言えます. しかし,マルチスレッド環境においてこのパターンを利用する際の 注意点については,あまり知られていません. まず,マルチスレッド環境において最も注意しなければいけないデッドロック について少し説明しましょう.デッドロックは,ロックを使って排他制御を行なう 場合に必ず考慮しなければいけない問題です.2つ以上のスレッドが互いにロック の解放を待ち合ってしまい,プログラムが動けなくなってしまう状況が デッドロックです. Java 言語では,java.lang.Object クラスが1オブジェクトにつき1つのロック を保持しています.このロックは「モニタ」と呼ばれます.Java 言語では, synchronized メソッドと synchronized ブロックによってそのロックの取得と 解放を行ない,複数のスレッド間の排他制御を実現しています. デッドロックが発生する簡単な状況は,2つのスレッドが2つのオブジェクトを 逆順にロックするケースです.例えば次のようなコードを考えます.
マルチスレッド環境でこの2つのクラスが動作し, スレッド1 が A オブジェクトのa1() を, 別のスレッド2 が B オブジェクトの b1() を呼び出した場合,タイミングによって デッドロックの可能性があります.スレッド1 はまずA オブジェクトのロックを取得し, 次に B オブジェクトのロックを取得します. 逆に,スレッド2 はまず B オブジェクトのロックを取得し, 次に A オブジェクトのロックを取得します. スレッド1 が A のロックを取得するとほぼ同時に スレッド2 が B のロックを取得した場合,スレッド1は B のロック解放を待ち, スレッド2 は A のロック解放を待つ,という状況に陥ります. 互いに相手の処理が進まないと前に進めないという典型的なデッドロックの状態です. マルチスレッドプログラミングになれた人にとって,クラスA と クラス B の コードを見れば,これが危険なプログラムであることは一目瞭然です.2つの クラス間に相互依存関係があり,互いに互いの synchronized メソッドを呼び出し ているからです. ところが,Observer パターンにおいてこの危険が常に潜んでいることにはなかなか気付きません. 実は,Observer パターンをマルチスレッド環境で使用する場合には,いつも この例のようなデッドロックの可能性があります. ConcreteSubject は自分の変更を受けて ConcreteObserver の変更通知 Update()メソッドを呼び出します.ところが,ConcreteObserver は ConcreteSubject についての知識があるため,いつでも別のスレッドが ConcreteSubject の GetState() メソッドを呼び出す可能性がありあす. 前述の逆転関係が,この潜在的なデッドロックの可能性を示唆しています. GetState() メソッドと Update() メソッドが,互いにロックを取得し合い, かつ,この2方向の呼び出しがスレッド間で交差すると,典型的なデッドロックとなります. 継承を持っていない class A と class B の例では,危険な構造が端的に コードに現れていました.ところが,Observer パターンでは,抽象クラス 側の呼び出し関係と具体クラス側の呼び出し関係に,この危険な構造が分割 されてしまっているため,ともすると発見しにくいデッドロックを生み出すことがあります. |
Observer パターンでのデッドロック回避 |
このようなデッドロックを回避するために,どのような対策が必要でしょうか. 一般的に,デッドロック問題を解決するにはロック階層と呼ばれるロックの順序 に関する取り決めをする方法が採られます.すなわち,2つ以上のロック資源が ある場合,各スレッドがロックを取得する順序をあらかじめ決めておくのです. すべてのスレッドが,規定された順序にしたがってロックを取得するという規約を 守れば,デッドロックは回避できます.次に,Observer パターンでのロックに関する規約を示します. ロックに関する規約 Observer パターンでは,Observer 側オブジェクト→ Subject 側 オブジェクトというロック取得順序を正順とし,この順序に従わないロック取得を禁止します.そのためには,
上記の取り決めは,オブジェクトレベルおよびスレッドレベルの取り決め であり,ランタイムの動作としての規約です. この取り決めは,4つのクラスの実装者が同じ場合は比較的一貫して守ること が可能です.しかし,一般には抽象Subjectと抽象Observerはフレームワー クとして提供され,ConcreteSubject と ConcreteObserverはアプリケーショ ンとして実装されます.この場合実装者も,開発時期も異なることが多いのです. よって,ランタイムの規約としてではなく,実装時の規定として, フレームワーク実装者,アプリケーション実装者の双方により具体的な指針が必要となります. 以下にその方針を挙げます. フレームワーク実装者への指針
アプリケーション実装者への指針
このような取り決めを守ることで,デッドロックを回避します. ロック階層の正順を,Observer 側→Subject側 としたのには理由が あります.アプリケーションでは一般に ConcreteObserver は ConcreteSubject に依存します.すなわち,ConcreteObserver 実装者は ConcreteSubject クラスに関する知識を持ちますから,ConcreteSuject の メソッドを利用することが頻繁に起こり得ます.よって,ConcreteObserver から ConcreteSubject という呼び出し順序をロック順として正順(自然な順序) とします.逆に,ConcreteSubject は一般にConcreteObserver に非依存であり, ConcreteSubject オブジェクトから ConcreteObserver オブジェクトへの 呼び出しは,抽象クラスのメソッドを経由してしか起こりません.よって, この順序を逆順とします.逆順の呼び出しに関しては,抽象クラスの実装者が ケアを行うことで,アプリケーション実装者の負担を減らすことができます. 次の章では,抽象クラスの実装者,すなわち多くの場合フレームワークの 実装者が,どのようにこの問題に対してケアを行っているかを見ましょう. |
Java の例 |
Java 言語では Observer パターンをそのまま java.util.Observer/Observableクラスで提供してます.また,java の イベ ントモデルは,Observer パターンのバリエーションとして捕らえることがで きます.すなわち,java イベントモデルにおいては,イベントソース側がイ ベント発火時(Subject の Notify() にあたる)に引数として明示的にイベント オブジェクトを渡すことで,イベントリスナ側の イベント発生時フック (Observer の Update() にあたる)に型付きの情報を渡すことができます.こ れを,push 型の Observer パターンと言います. java が提供するイベントモデルでは,イベントソース側の 抽象クラスは明確に定義されておらず,イベントリスナ側の抽象クラスも java.util.EventListener がマーカーインターフェイス(空のインターフェイス) として用意されているだけで,特にクラスライブラリとしてのサポートは ありません.しかし,命名則を伴った1対のコーディングパターンとして awt, javaBeans をはじめ java クラスライブラリのいたるところで使用されています. 表1に GoF の Observer パターン,java.util.Observer/Observable, java イベントモデルの対応を挙げてみます. |
GoF の Observerパターン |
java.util.Observer /Observable |
java Event モデル (各メソッドの名前は例) |
|||
---|---|---|---|---|---|
Subject | Attach() | Observable | addObserver() | EventSource | addEventListener() |
Detach() | deleteObserver() | removeEventListener() | |||
Notify() | notifyObservers() | fireEvent() | |||
Observer | Update() | Observer | update() | EventListener | eventHappened() |
さて,フレームワーク実装者が,デッドロック回避のためのケアを どのように行っているかを,コーディング例で見てみます.以下のコード は,Subject クラスの Notify() メソッドに対応する,java.util.Observable の notifyObserver() メソッドを簡略化し,コメントを付けたものです. コメント中,/* .... */ のコメントは,原ソースコードのコメントをそのまま意訳したものです. イベントソースの例である,java.beans.PropertyChangeSupport クラスでも, これと同様の考慮が施されています. |
class Observable { ..... private Vector obs; // Observer 達 private boolean changed = false; // このオブジェクトの変化フラグ ..... public void notifyObservers() { // このメソッド自体は synchronized メソッドではない! Object[] arrLocal; // obs のスタック上のコピー synchronized (this) { // 原ソースコード中コメントの和訳 /* Observer がロックを保持したまま,このオブジェクトの * 任意のコードにコールバックしてくるのは望ましくない. * この実装では,Vector からそれぞれの Observer を * 取り出し,Observable の状態(changed)を参照・更新する * ところまでは同期化(synchronization)の必要があるが, * 実際に通知を行う部分では同期化の必要はない * (同期化してはいけない). * この実装での潜在的な競合条件による結果は,最悪でも, * 1) 新しく登録された Observer が現在進行中の notify の * 受け取りを逃してしまう. * 2) 最近削除された Observer が,もう関心がない * notify を誤って受け取る. * の2つである. */ // 変更がなければ終了 if (!changed) return; // オブジェクト変数をスタック上(スレッド毎の資源)にコピー arrLocal = obs.toArray(); // 変更フラグを更新 changed = false; } // observer オブジェクトへの呼び出しは同期化の外 for (int i = arrLocal.length-1; i < =0; i--) ((Observer)arrLocal[i]).update(this); } ....... } |
図7 のコード例では,Subject の実装者がいかに慎重な考慮を払って Notify() メソッドを実装しているかがわかります. このような Subject 実装者の考慮があっても,サブクラスであるConcreteSubject の実装が,synchronized ブロックの中でNotify() を 呼び出してしまうような 規約違反があれば,すべての苦労は水の泡となることは言うまでもありません. ちなみに,ConcreteSubject が自身の synchronized メソッドの中でNotify() を呼び出す のは,最もよく起こるコーディングミスの1つです.イベントモデルに おいて,イベントソースが自身の synchronized メソッドの中で発火メソッドを 呼び出すのも全く同じコーディングミスです. |
おわりに |
この記事では,有名なデザインパターンである Observer パターンを例に, その中に潜むマルチスレッド環境での注意点について考察しました. ここで述べられている議論は,C++ 言語で java ライクなマルチスレッド プログラミングを実現するクラスライブラリ,ThreadJack(スレッドジャック) を開発中に掘り下げられたものです.最初にこのアイディアに気づき,実装を 行った(株)永和システムマネジメントの徳井進氏に感謝します. ThreadJack は POSIX スレッド, UI スレッド, Win32 スレッド, VxWorks 上に Java のスレッドモデルを実現しています.ThreadJack は非営利利用に おいてはソースコードも含めて無料で公開されています.以下の URL を参照してください. Kenji Hiranabe <hiranabe@esm.co.jp> |