ツールボックスの安全な利用方法
ご存じの通り、Mac OS とツールボックス (ここからは 2 つをまとめて簡単に「ツールボックス」と言うことにします) は 1980 年代の初めに、一度に 1 つのアプリケーションを実行し、そのアプリケーションの 1 つのスレッドしか実行できないマシンを対象に設計されたものです。それ以後多少の進歩はありましたが、この基本的な設計の制約のため、未だにツールボックスはシングルスレッド対応として使う必要があります。Mac では複数アプリケーション間の協調的マルチタスクが可能ですが、これは、既定の時点 (WaitNextEvent の呼び出し) でしかプロセス切り替えを行うことができず、下位メモリに保持された大域的状態を完全にスワップしなければなりません。
このため、Mac OS アプリケーションで複数スレッドを実行しようとすると問題が起こります。ツールボックスの呼び出しが再入可能でないとは、1 つのスレッドがツールボックス呼び出しを実行している間は他のスレッドがツールボックスに入れないという意味です。さらに、全般的に大域的状態 (現在の GrafPort やリソースチェーンなど) が使用されているため、その状態に依存する一連の呼び出しを行う必要のあるスレッドは、その間、他のスレッドがその状態を利用したり変更するのを防ぐ必要があります。
われわれは MRJ 2.1 用 AWT の実装でこの問題に対処しなくてはなりませんでした。AWT の大部分が、JDirect2 を通じてツールボックスを呼び出す Java のコードで書かれていたためです。われわれの取った解決策はごく当たり前の「クリティカルセクション」を使用するもので、ツールボックスを使用するコード部分をクリティカルセクションとし、一時に 1 つのスレッドだけしかそのセクションに入れないようにするというものです。
Java の用語でいうと、この問題は大域オブジェクト (public および static な変数を通してアクセスする) を 1 個用意して、これを同期のためのロックとして働かせることで実現します。われわれはすべてのクリティカルセクションをそのオブジェクトで同期を取る Java ブロックに入れることで対処しました。そのオブジェクトとは次のものです。
com.apple.mrj.macos.toolbox.Toolbox.LOCK
(これは、com.apple.mrj.macos.toolbox パッケージの Toolbox クラスの LOCK という名前の static で final な変数です。) この変数の使い方は、適切なインポート命令があるものと仮定すると、次のようになります。
synchronized( Toolbox.LOCK ) {
...
}
われわれ MRJ チームはこのことを「LOCKing (LOCK する)」とか「using the LOCK (LOCK を使う)」と呼んでいます。(英語で発音するときは「LOCK」の部分をそれが大文字であることがわかるよう強く言います。)
なぜこれが必要か
われわれは AWT そのものを正常に動作させるためにこの方法を採用しました。しかし、他のデベロッパが JDirect を使うコードを書き (読者の大半がそうでしょう)、そのコードを、同時に AWT も使用するアプリケーションで使用する場合、同じ同期テクニックを使って、読者のコードとアップルのコードの干渉を防ぐ必要があります。このような理由で、このテクニカルノートを作成しました。
サンプル
完全に同期の取れた SysBeep への呼び出しを行う簡単なサンプルを掲載します。
import com.apple.mrj.macos.toolbox.Toolbox;
import SoundFunctions; // SDK の JDirect サンプルコードより
...
public void playABeep( ) {
synchronized(Toolbox.LOCK) {
SoundFunctions.SysBeep(1);
}
}
|
ネイティブコード (JNI)
これは JDirect だけの問題ではありません。ネイティブメソッドを作成して (おそらく JNI を使用して) ツールボックスの呼び出しを行う場合、同じ問題が起こる可能性があります。ネイティブコードで、現在のポートや GrafPort の内部状態など、ツールボックスの状態を変える場合、その動作は LOCK で同期を取らなければなりません。一番簡単な方法は、ネイティブコードはそのままにして、ネイティブメソッドを呼び出しているところをすべて必ず LOCK することです。しかし、JNI API で LOCK オブジェクトを取得し、モニタを取得/解放することもできます。この場合、ネイティブメソッドから抜ける前に必ずモニタを解放するようくれぐれも注意してください。これを怠ると MRJ がハングします。
先頭ページに戻る
現実的な対応方法
実際は、上記の警告は現時点では厳しすぎます。本当は、今のところツールボックスへの呼び出しを 1 つ残らず LOCK する必要はありません。さっきの SysBeep のように、システムの状態変更を求めず行いもしない呼び出しは現在のところ同期を必要としません。これは、MRJ 2.1 のスレッドが本来の意味でプリエンプティブでなく、Java コードのみをタイムスライスして、そう見せかけているだけだからです。ツールボックスを含め、ネイティブコードの実行はプリエンプティブになりません。ツールボックスへの呼び出しはどれも必要なだけ最後まで実行されます。他の Java スレッドが割り込むことはなく、別のスレッドによるツールボックスへの再入の危険はありません。
しかし、ここで「今のところ」と書いたことに注意してください。現状がこうなっているのは、Mac OS では、システム心臓部のナノカーネルの制約から、スレッドが正しく実装できていないためです。Mac OS 8 (別名「ブルー」) の将来のバージョンでは、ナノカーネルが改善されるため、将来的には本物のプリエンプティブな実行環境がサポートされる可能性があります。さらに、Mac OS X は、元から本物のプリエンプティブスレッドをサポートしている Mach 3 マイクロカーネルが基盤となる予定で、Java はこれを使用します。
このことから、あらゆるネイティブメソッドコードで変更が必要になるでしょう (Mac OS のコードはすべて、OS X の Carbon API で動作するように改訂する必要があるため、これは驚くことではありません)。OS X では、ネイティブメソッドが他のスレッドによって割り込まれる可能性があるため、同期がいっそう重要になってきます。デベロッパは、Java コードで行ったのと同じように、ネイティブメソッドの内部でも LOCK の取得/解放を行わなければならなくなります。Mac OS X がもう少し具体化したら、同期のためにどのような作業が必要かについて詳しい情報を提供することにします。
したがって、JDirect または JNI 経由のツールボックス呼び出しを Mac OS の将来のバージョンで実装された Java でも動作させるためには、今からただちにすべてのツールボックス呼び出しを LOCK するようにしてください。律義にすべてを LOCK している時間がない場合は、少なくとも、大域環境への干渉が問題となる一連のツールボックス呼び出しだけは必ず LOCK するようにしてください。
先頭ページに戻る
「LOCK する」ことの危険
残念ながら、マルチスレッドプログラミングの経験者ならだれもが言うように、こうした同期にはそれ自体危険が伴います。基本的なものとしては、昔から知られたデッドロックという問題があります。
デッドロックについての簡単な説明
デッドロックが発生するのは、あるモニタを保持する (Java で言えば synchronized に挟まれたブロックを実行中の) あるスレッドが、別のスレッドが保持する別のモニタを取得しようとしており、しかも第 2 のスレッドは、最初のスレッドが保持しているモニタを取得しようとしてすでにブロックしている、という状態になった場合です。わかりやすくいいかえると「あなたが私に缶切りを渡さない限り缶詰めを渡すことはできないが、あなたは私が缶詰めを渡すまで缶切りを渡そうとしないので、あなたに缶詰めを渡すことはできない」ということです。これでは二人とも食事ができません。Java の場合は、両方のスレッドが永久にブロックします。
LOCK オブジェクトについても、この点では何も特別なことはありません。LOCK は多数の同期対象となるため、デッドロックが起こらないか特に注意しなければなりません。デッドロックを起こす典型的な状況を次に示します。
public synchronized void foo( ) {
System.out.println("In foo!");
synchronized(Toolbox.LOCK) {
SoundFunctions.SysBeep(1);
}
}
public void bar( ) {
synchronized(Toolbox.LOCK) {
SoundFunctions.SysBeep(1);
foo();
}
}
|
さしさわりのあるコードには見えないかもしれませんが、foo を呼び出したスレッドが println まで来て割り込まれた場合を考えてください。ここで制御は同じオブジェクトの bar メソッドに切り替わります。2 番目のスレッドは bar メソッドの実行を進め、foo を呼び出します。最初のスレッドはすでにそのオブジェクトのモニタを保持しているため (foo で同期を取るため)、2 番めのスレッドもブロックします。
しばらくして、最初のスレッドが再開され、foo メソッドの synchronized 文に入ろうとします。残念ながら、2 番目のスレッドがすでに LOCK を保持しているため、最初のスレッドもブロックしてしまいます。
これで両方のスレッドがブロックしてしまい、どちらかがモニタを解放しない限り、どちらのスレッドも先へ進むことができません。永久にデッドロックです。
回避方法
デッドロックを防ぐ模範的な回避策の 1 つは、モニタの取得を常に同じ順序で行うことです。上のサンプルでデッドロックが起こる原因は、最初のスレッドが先に、受け取ったオブジェクトのロックを取得してから、ツールボックスの LOCK を取得しているのに対して、2 番目のスレッドは逆の順序でロックを取得するためです。
われわれのコードでは、モニタの取得順序として、常に LOCK を一番最後に取得する、という方針を取りました。Java 言語で言うと、LOCK に同期している間は、他の何かに同期したり、他の何かに同期するメソッドを呼び出してはならないということです。(上のサンプルでは、bar メソッドが foo を呼び出すときにこのルールが破られています。最良の解決法は foo への呼び出しを同期ブロックの外に移すことでしょう。)
具体的に言うと、この必然の結果として、LOCK を保持している間は AWT を呼び出してはならないということが重要になります。これは、public な AWT メソッドはもちらん、それらが呼び出す private なピアクラスも、多数の同期を行うためです。
うまくこなせるとお感じの場合は、上記の規則に多少の例外を設けることができます。あるオブジェクトに同期している間、決して LOCK を行わないことがわかっている場合は、LOCK したブロックからそのオブジェクトに同期するメソッドを呼び出してもかまいません。例えば、Vector オブジェクトの大半のメソッドは同期を行いますが、LOCK されたブロック内から Vector オブジェクトのメソッドを呼び出しても大丈夫でしょう。ほかにその Vector に同期を取るコードがある可能性が非常に少ないからです。
この結果、次のようなプログラミングスタイルが導き出されます。LOCK のブロックをツールボックス呼び出しにできるだけきつく被せる方式です。まずツールボックスを呼び出して、次に Java で他の処理を行い、それからもう一度ツールボックスを呼び出す必要のあるメソッドがあるとします。この場合、メソッド全体を LOCK してはいけません。そうではなく、ツールボックスを呼び出す最初のグループと 2 番目のグループをそれぞれ独立に LOCK し、その間の処理は LOCK ブロックには入れません。こうなると、最初のグループと 2 番目のグループとでは大域的な状態が変化する可能性があります。変更されては困る場合は、2 番目のグループの先頭で状態を設定し直すか、何とか 2 つのグループを 1 つにまとめて、間の Java コードを前か後ろに追い出せるよう、コードの順序を変えることができないか検討してみることです。
デッドロックのデバッグ
われわれは MRJ 開発の際にひどくデッドロックに悩まされたため、デッドロックのデバッグサポートを MRJ VM に追加しました。
まず、MRJLib のデバッグビルドでは、スレッドスケジューラにデッドロック検知機能を入れました。この機能は、上記で説明した典型的なデッドロックのケースを検知するもので、デッドロックを検知すると、それを通知するユーザブレイクで即座に MacsBug に入ります。それ以後は、以下で説明するテクニックを使って詳しい情報を得ることができます。(このデバッグビルドは MRJ 2.1 SDK に付属します。インストール方法や詳細事項については、付属の Read-Me ファイルを参照してください。)
通常の最適化ビルドの MRJLib でも、MRJ の 'dcmd' がインストールされていれば、MacsBug を使用し、 'mrj dl' および 'mrj sync' コマンドで、現在デッドロックまたはデッドロックに近い同期の問題が起こっているかを調べることができます。この 2 つのコマンドは、問題のスレッドやオブジェクトの情報を表示します。( 'dcmd' は MRJ 2.1 SDK に付属します。詳細情報と具体的なコマンドの用法については、付属の Read-Me ファイルと「Technote 1154: MacsBug で Java コードをデバッグする方法」を参照してください。)
先頭ページに戻る
参考文献
- 「Technote 1154: MacsBug で Java コードをデバッグする方法」
- 「Using JDirect To Access MacOS Code From Java」
JDirect の簡単な手引き。MRJ 2.1 SDK の「docs」フォルダに収録。
- Oaks、Scott、Wong 著『Java Threads』O'Reilly 刊。1997 年。
O'Reilly の優れた書籍で、Java のマルチスレッドおよびスレッドの手引書として最適。初心者が読みやすく書かれているが、上級ユーザのためになる項目も掲載。
- Lea、Doug 著『Concurrent Programming in Java: Design Principles and Patterns』Addison-Wesley 刊。1997 年。
非常に優れた、内容の充実した書籍だが、アカデミックなためもてあます読者もいるかもしれません。実は私自身まだ全部読みきっていません。
先頭ページに戻る
|