コールバック機構を利用して並行処理時に発生するバグを防止するはじめにJavaプログラマが最初にマルチスレッドプログラミングを学ぶときに犯す間違いの1つに、ロックについての誤解があります。つまりオブジェクトをロックすれば、該当オブジェクトのフィールドやメソッドへのアクセスを防止できると考えがちなのですが、実際に行われるのは、他のスレッドが同一のロックを得るのを禁止するだけです。こうした誤解もわからなくはないのですが、これを勘違いしたままだと厄介な並行処理のバグを引き起こす可能性があります。 実際、多くの並行処理バグは、特定のデータが間違ったタイミングでよそからアクセスされた場合に起こるものであり、通常は何らかの変更を加えようとした場合がこれに相当します。一般の並行処理モデルでは、複数のソースファイルが関与する同期ゾーンをセットにして利用しています。これは微妙な相互依存関係の下で成り立つデザインであり、コードに変更を加えるような場合、異なる要素間の微妙な関係を管理するのが困難になり、「並行処理の崩壊」という現象に遭遇しやすくなります。ということは逆に、オブジェクトを真の意味でロックすることができ、自分だけが操作できる状況にすることができれば、非常に有用なのではないでしょうか? 本稿では、高負荷の並列サーバーで並行処理の崩壊を起こさないようにするための方法を解説します。これは、すべてのデータアクセスをコールバック機構を通じて行うように制限することで、サーバーの並行処理を一箇所にまとめるという方式です。これにより、並行処理制約に対する違反を簡単に特定できるようになります。 コールバックによるシングルスレッドアクセスこのチュートリアルでは、コールバックを用いてシングルスレッドアクセスを実現するサーバーの設計・構築方法を解説します。ここでは、機密データを含んでいるオブジェクトに対して「リニアなアクセス」が行われるようにします。つまり、同時にアクセスできるスレッドを1本だけに制限します。 本稿では、通常用いられているものとはまったく逆の手法でこれを実現します。通常のケースでは、複数のスレッドが同一のリソースにアクセスしようとする場合は、ロックを争うことになります。つまりロックを獲得したスレッドがそのアクセス権を独占し、必要な処理が終わった時点でロックを解放し、別のスレッドがアクセス権を得るという流れを取ります。 本稿のデザインではコールバックを使用します。まず、コールバックオブジェクトを こうして見ると、このデザインは、通常の方式と根本的に異なっているわけではありません。各々のオブジェクトやスレッドは順番待ちをして、自分の番が来れば排他的なアクセスを行います。ただし、コントロール権を求めてスレッド同士を競わせるのではなく、コントロール権をゲートキーパーオブジェクトにゆだねて、誰がアクセスできるかをこのゲートキーパーで判断させる点が異なります。 コールバックを用いるメリットコールバックベースのシステムの実装には余分な作業を必要としますが、労力に見合うだけのメリットがあります。 通常の手法では、特定のデータにアクセスするには、必要なロックを事前に取得しなければなりません。同期メソッドを採用すればこうした処理を義務付けることはできますが、それでもやはりアクセス側のコードは同期プロトコルに従わなければならないので、この処置だけでは不十分なケースが生じてきます。たとえばシステムが巨大化するにつれて、行うべきではないタイミングでのデータアクセスを行いやすくなります。また、同期ブロックを設置すると、その分だけデッドロックが発生する危険性が高くなります。 これに対してコールバックを用いた方式では、データがメソッドに渡されて初めて該当データにアクセスできるようになり、このメソッドが終了すると、該当データへのアクセスはできなくなります。このように、データを利用できる期間が明確に特定されるのがこの方式のメリットです。 またこの方式では、ゲートキーパーによる制御も加わるので、これを利用して任意のオーダリングメカニズムを実装することもできます。この点、従来の待機/通知方式では、どのスレッドがどのタイミングでアクセス権を得るかを確認するための手段が存在しません。 補足
コールバック方式では、機密データのポインタを隠匿するのも、別のオブジェクトやスレッドに渡すのも自由です。これはリニア性の保証に対する明確な違反ですが、偶発的に行ってしまうような処理ではありません。
Sumクラス まず、今回のスレッド制御システムのアクセス対象となる機密データオブジェクトを作成します。ここでは public class Sum { public int a, b, c; // ... } もちろん同期アクセス方式でも、変数 GateKeeperクラス GateKeeper gk = new GateKeeper( new Sum() );
gk.use( user );
User、Accessor、Mutator インターフェースJavaにはファーストクラスの関数が存在しないので、本当の意味でのコールバックを使うことはできません。ただし、インターフェースを用いることで、コールバックに類似した機能を実現することができます。 public interface Accessor extends User { public void access( Object o ); } 同様に public interface Mutator extends User { public void mutate( Object o ); } その他に、 public interface User { } また、 public interface MutatingAccessor extends Accessor, Mutator { } use()の実装 処理の中心となる public void use( User user ) {
if (user instanceof Mutator) {
Mutator mutator = (Mutator)user;
これで、
try {
rwlock.getWriteLock(); // LOCK
mutator.mutate( o );
} finally {
rwlock.releaseWriteLock(); // UNLOCK
}
こうして見ると、コールバック方式は従来型のロック/アクセス/アンロックのパターンと基本的に変わらないことが理解できるでしょう。ただしここでのロック処理は、ゲートキーパーがすべてを掌握しています。こうした方式には、いくつかの大きなメリットがあります。
高可用性サーバーにとって最も重要な意味を持つのは、4番目のメリットでしょう。通常、マルチスレッドのデータ構造を扱う場合、すべてのクライアントがロックおよびアンロック処理に気配りをしなければなりませんが、これはまた、リソースをロックしたままアンロックをし忘れるという失敗を、誰もが犯し得ることを意味します。プログラマであれば誰でもこうしたミスを避けようとするでしょうが、実際にこうした事態はときどき発生してしまいます。この「ときどき」という発生率が、高可用性サーバーにとって容認しがたい頻度であれば、より強力な防止策を講じる必要があります。 先のコード部では、
if (user instanceof Accessor) {
Accessor accessor = (Accessor)user;
このコードは先に見た
try {
rwlock.getReadLock();
accessor.access( o );
} finally {
rwlock.releaseReadLock();
}
読み込みロックは排他的ではないので、この構成は、該当データを同時に複数のリーダーが読み込むことを許可します。同時に、データへの書き込みを行うスレッドについては、完全に排他的なアクセス持つことになります。 Poundクラス(GateKeeperのテスト) 今回の同期手法の中心は こうしたテストを行ってくれるのが % java Pound 20 この場合は、20個の 各 public void mutate( Object o ) { Sum sum = (Sum)o; 最初に、
// Change a or b.
int delta = rand.nextInt( 2000 ) - 1000;
if (rand.nextInt( 2 )==0) {
sum.a += delta;
} else {
sum.b += delta;
}
この処理の実行直後は、 この点を確認するために、
// The better to stress the thread-safety of the system.
Thread.yield();
try { Thread.sleep( 20 ); } catch( InterruptedException ie ) {}
一時停止した後に、データの不整合を修正して処理を続けます。
// Make sum correct again.
sum.c = sum.a + sum.b;
// Report.
checkAndReport( sum, "mutate" );
pause();
}
Sum sum = (Sum)o;
// Just check the sum.
checkAndReport( sum, "access" );
pause();
唯一行うべきことは、値を出力して、 コードの簡単なテストとしては、ある程度の数のスレッド(10から20本くらい)を使って、 オブジェクトを使用するスレッド数の追跡筆者がこのテクニックを開発したのは、以前にJavaのガベージコレクタの扱いに苦労したことがあったからです。あるときメモリ残量がなくなりかけたのですが、メモリはネイティブコードで割り当てられていたので、ガベージコレクタが働いてくれませんでした。処分してほしい巨大なオブジェクトがいくつかあったのですが、Javaはそれが巨大だとは認識しないので、そのままになっていました。 ここで本当に必要だったのは、特定のオブジェクトがいつ不要になったのかを知る方法なのですが、Javaではこれが隠されています。そもそもガベージコレクタの存在意義は、こうしたオブジェクトを自動的に処分してくれることです。 リニア変数やモナディックステート(monadic state)という考え方にヒントを得てたどり着いたのが、シングルスレッドあるいはリニアオブジェクトという、同時にアクセスできるのが単一のスレッドだけとなるようにすればいいという考え方でした(ここの解説では、複数のリーダーを許すよう拡張してありますが)。こうした構造を用いると、同一のオブジェクトを使用するスレッドの数をより細かく制御・追跡できるようになりますし、該当オブジェクトが不要になった段階で知りたいのはこうした情報なのです。 著者紹介Greg Travis(Greg Travis)
ニューヨーク在住のJavaプログラマ兼テクノロジーライター。ハイエンドPCゲーム業界で3年間を過ごした後に、EarthWebに参加し、当時最新鋭のJavaプログラミング言語を用いた新規テクノロジーを各種開発。1997年以降は、さまざまなWebテクノロジーについてのコンサルタントを務める。
関連記事 関連テーマ 最新トップニュース
|
|