japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2008年10月8日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2005年9月27日 10:00

コールバック機構を利用して並行処理時に発生するバグを防止する

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!

はじめに

 Javaプログラマが最初にマルチスレッドプログラミングを学ぶときに犯す間違いの1つに、ロックについての誤解があります。つまりオブジェクトをロックすれば、該当オブジェクトのフィールドやメソッドへのアクセスを防止できると考えがちなのですが、実際に行われるのは、他のスレッドが同一のロックを得るのを禁止するだけです。こうした誤解もわからなくはないのですが、これを勘違いしたままだと厄介な並行処理のバグを引き起こす可能性があります。

 実際、多くの並行処理バグは、特定のデータが間違ったタイミングでよそからアクセスされた場合に起こるものであり、通常は何らかの変更を加えようとした場合がこれに相当します。一般の並行処理モデルでは、複数のソースファイルが関与する同期ゾーンをセットにして利用しています。これは微妙な相互依存関係の下で成り立つデザインであり、コードに変更を加えるような場合、異なる要素間の微妙な関係を管理するのが困難になり、「並行処理の崩壊」という現象に遭遇しやすくなります。ということは逆に、オブジェクトを真の意味でロックすることができ、自分だけが操作できる状況にすることができれば、非常に有用なのではないでしょうか?

 本稿では、高負荷の並列サーバーで並行処理の崩壊を起こさないようにするための方法を解説します。これは、すべてのデータアクセスをコールバック機構を通じて行うように制限することで、サーバーの並行処理を一箇所にまとめるという方式です。これにより、並行処理制約に対する違反を簡単に特定できるようになります。

コールバックによるシングルスレッドアクセス

 このチュートリアルでは、コールバックを用いてシングルスレッドアクセスを実現するサーバーの設計・構築方法を解説します。ここでは、機密データを含んでいるオブジェクトに対して「リニアなアクセス」が行われるようにします。つまり、同時にアクセスできるスレッドを1本だけに制限します。

 本稿では、通常用いられているものとはまったく逆の手法でこれを実現します。通常のケースでは、複数のスレッドが同一のリソースにアクセスしようとする場合は、ロックを争うことになります。つまりロックを獲得したスレッドがそのアクセス権を独占し、必要な処理が終わった時点でロックを解放し、別のスレッドがアクセス権を得るという流れを取ります。

 本稿のデザインではコールバックを使用します。まず、コールバックオブジェクトをaccess()というメソッドを用いて「ゲートキーパー」に渡します。ゲートキーパーは、このコールバックオブジェクトのメソッドに機密データを渡します。コールバックオブジェクトがデータを使い終わると、ゲートキーパーが次のコールバックオブジェクトにデータを渡します。

 こうして見ると、このデザインは、通常の方式と根本的に異なっているわけではありません。各々のオブジェクトやスレッドは順番待ちをして、自分の番が来れば排他的なアクセスを行います。ただし、コントロール権を求めてスレッド同士を競わせるのではなく、コントロール権をゲートキーパーオブジェクトにゆだねて、誰がアクセスできるかをこのゲートキーパーで判断させる点が異なります。

コールバックを用いるメリット

 コールバックベースのシステムの実装には余分な作業を必要としますが、労力に見合うだけのメリットがあります。

 通常の手法では、特定のデータにアクセスするには、必要なロックを事前に取得しなければなりません。同期メソッドを採用すればこうした処理を義務付けることはできますが、それでもやはりアクセス側のコードは同期プロトコルに従わなければならないので、この処置だけでは不十分なケースが生じてきます。たとえばシステムが巨大化するにつれて、行うべきではないタイミングでのデータアクセスを行いやすくなります。また、同期ブロックを設置すると、その分だけデッドロックが発生する危険性が高くなります。

 これに対してコールバックを用いた方式では、データがメソッドに渡されて初めて該当データにアクセスできるようになり、このメソッドが終了すると、該当データへのアクセスはできなくなります。このように、データを利用できる期間が明確に特定されるのがこの方式のメリットです。

 またこの方式では、ゲートキーパーによる制御も加わるので、これを利用して任意のオーダリングメカニズムを実装することもできます。この点、従来の待機/通知方式では、どのスレッドがどのタイミングでアクセス権を得るかを確認するための手段が存在しません。

補足
 コールバック方式では、機密データのポインタを隠匿するのも、別のオブジェクトやスレッドに渡すのも自由です。これはリニア性の保証に対する明確な違反ですが、偶発的に行ってしまうような処理ではありません。

Sumクラス

 まず、今回のスレッド制御システムのアクセス対象となる機密データオブジェクトを作成します。ここではSumという名前の単純なデータオブジェクトを作成することにします。

public class Sum
{
  public int a, b, c;
  // ...
}

 Sumには、c = a + bという関係にあるabcという3つの数値を格納します。これらはパブリック変数なので、誰でもその値を変更することができ、c = a + bという関係が崩れる場合も生じてきます。ここでは、こうした状況の防止策を施すようにします。

 もちろん同期アクセス方式でも、変数cを隠して保護することはできます。しかし本稿の目的は、通常の同期方式の利用が困難または適していない状況において利用できる、データ保護の代替手法を確認することです。Sumは極めて単純なオブジェクトですが、ここでの関心はデータ構造の複雑さにあるのではなく、脆弱なデータをどう扱うかに注目しています。

GateKeeperクラス

 Sumオブジェクトへのアクセスは、すべてGateKeeperクラスを経由します。GateKeeperは、そのコンストラクタに渡されるオブジェクトのアクセスをコントロールします。

  GateKeeper gk = new GateKeeper( new Sum() );

 GateKeeper中に隠されているオブジェクトを使用するには、GateKeeperuse()メソッドにコールバックを渡す必要があります。

    gk.use( user );

User、Accessor、Mutator インターフェース

 Javaにはファーストクラスの関数が存在しないので、本当の意味でのコールバックを使うことはできません。ただし、インターフェースを用いることで、コールバックに類似した機能を実現することができます。

 Accessorインターフェースでは、データを読み取るオブジェクトを指定します。

public interface Accessor extends User
{
  public void access( Object o );
}

 同様にMutatorインターフェースでは、データを書き出すオブジェクトを指定します。

public interface Mutator extends User
{
  public void mutate( Object o );
}

 その他に、Userという空のインターフェースも定義する必要があります。Userを実装するオブジェクトは、AccessorsまたはMutatorsのいずれかになります。

public interface User
{
}

 また、AccessorMutatorの両方を実装するMutatingAccessorというインターフェースも用意しておきます。これは必須ではありませんが、これによってオブジェクト宣言を簡潔化することができます。

public interface MutatingAccessor extends Accessor, Mutator
{
}

use()の実装

 処理の中心となるGateKeeperクラスのuse()メソッドを詳しく見ておきましょう。use()のパラメータはUser型なので、AccessorMutatorまたはその両方のオブジェクトを渡すことができます。

  public void use( User user ) {

 userMutatorAccessorのどちらにもなり得るので、どちらの側面を先に扱うのかを決める必要があります。このチュートリアルでは、Mutatorの方を先にします。

    if (user instanceof Mutator) {
      Mutator mutator = (Mutator)user;

 これで、userMutatorであることが分かります。しかしMutatorに変更を行わせるには、事前にロックを取得しておく必要があります。そのためには、変更処理をロック/アンロックのペアで囲みます。

      try {
        rwlock.getWriteLock();       // LOCK

        mutator.mutate( o );

      } finally {
        rwlock.releaseWriteLock();   // UNLOCK
      }

 こうして見ると、コールバック方式は従来型のロック/アクセス/アンロックのパターンと基本的に変わらないことが理解できるでしょう。ただしここでのロック処理は、ゲートキーパーがすべてを掌握しています。こうした方式には、いくつかの大きなメリットがあります。

  1. ロックとアンロックの構造が非常に明確になる。
  2. ゲートキーパーがロックのポリシーを決定できる。
  3. クライアントコードが大幅に簡単化される。
  4. ロックとアンロックの処理が1箇所だけで行われる。

 高可用性サーバーにとって最も重要な意味を持つのは、4番目のメリットでしょう。通常、マルチスレッドのデータ構造を扱う場合、すべてのクライアントがロックおよびアンロック処理に気配りをしなければなりませんが、これはまた、リソースをロックしたままアンロックをし忘れるという失敗を、誰もが犯し得ることを意味します。プログラマであれば誰でもこうしたミスを避けようとするでしょうが、実際にこうした事態はときどき発生してしまいます。この「ときどき」という発生率が、高可用性サーバーにとって容認しがたい頻度であれば、より強力な防止策を講じる必要があります。

 先のコード部では、finallyブロックを使って、状況の如何にかかわらず、リソースを確実にアンロックしています。

 Mutatorによる変更は実施できるようにしたので、今度はAccessorによるアクセスも実行できるようにしなければなりません。Mutatorはデータの変更を行えますが、Accessorに許可されているのはデータの読み込みだけです。

    if (user instanceof Accessor) {
      Accessor accessor = (Accessor)user;

 このコードは先に見たMutatorのものと非常によく似ていますが、読み込みロックのみが必要で、書き込みロックは不要である点が異なります。

      try {
        rwlock.getReadLock();
        
        accessor.access( o );

      } finally {
        rwlock.releaseReadLock();
      }

 読み込みロックは排他的ではないので、この構成は、該当データを同時に複数のリーダーが読み込むことを許可します。同時に、データへの書き込みを行うスレッドについては、完全に排他的なアクセス持つことになります。

Poundクラス(GateKeeperのテスト)

 今回の同期手法の中心はGateKeeperであり、GateKeeperの根幹を成しているのがuse()メソッドです。use()メソッドは、非常に短く構造も単純なので、一見して、行うべき処理を忠実に実行するはずのように思われます。たとえそうであっても、一通りのテストは施しておく必要があります。

 こうしたテストを行ってくれるのがPoundクラスです。その名前から連想されるようにPoundは、多数のスレッドを用いて、可能な限りの高速でGateKeeperを「連打」します。当然ながら、このGateKeeper内部にはSumオブジェクトを入れておく必要があります。

 Poundを実行するには次のようにします。

% java Pound 20

 この場合は、20個のPoundオブジェクトが作成され、20本のスレッドで動作します。各々のPoundオブジェクトは、自分の順番が来るたびに、Sumオブジェクトに対する変更と検証の処理を行います。

 各Poundは、Sumオブジェクトにアクセスする際にGateKeeperを通過する必要があります。つまり、これらはAccessorおよびMutatorになります。メインループを一巡するごとにPoundオブジェクトは、アクセス(読み込み)または変更(書き込み)を行います。mutate()メソッドのコードを次に示します。

  public void mutate( Object o ) {
    Sum sum = (Sum)o;

 最初に、aまたはbを変更します

    // Change a or b.
    int delta = rand.nextInt( 2000 ) - 1000;
    if (rand.nextInt( 2 )==0) {
      sum.a += delta;
    } else {
      sum.b += delta;
    }

 この処理の実行直後は、c = a + bという関係が崩れています。ここで誰かがこのデータにアクセスするのは、できれば避けたい事態です。

 この点を確認するために、yield()sleep()を実行し、別のスレッドを実行できるようにします。仮にコードが正しく機能したとしても、それは別のスレッドがアクセスしなかったからだという場合もあり得るので、そうした可能性を排除して、コードそのものに問題がないことを確認すべきだからです。

    // 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();
  }

 mutate()呼び出しの終了後、他のスレッドが実行できるようになります。access()メソッドはもっと単純です。

    Sum sum = (Sum)o;

    // Just check the sum.
    checkAndReport( sum, "access" );

    pause();

 唯一行うべきことは、値を出力して、c = a + bという関係が実際に成立しているかを確認することです。すべてが正しく動作しているならば、そうした結果が得られるはずです。

 コードの簡単なテストとしては、ある程度の数のスレッド(10から20本くらい)を使って、Poundをしばらく実行し続けてください。それで、エラーが報告されなければOKです。

オブジェクトを使用するスレッド数の追跡

 筆者がこのテクニックを開発したのは、以前にJavaのガベージコレクタの扱いに苦労したことがあったからです。あるときメモリ残量がなくなりかけたのですが、メモリはネイティブコードで割り当てられていたので、ガベージコレクタが働いてくれませんでした。処分してほしい巨大なオブジェクトがいくつかあったのですが、Javaはそれが巨大だとは認識しないので、そのままになっていました。

 ここで本当に必要だったのは、特定のオブジェクトがいつ不要になったのかを知る方法なのですが、Javaではこれが隠されています。そもそもガベージコレクタの存在意義は、こうしたオブジェクトを自動的に処分してくれることです。

 リニア変数モナディックステート(monadic state)という考え方にヒントを得てたどり着いたのが、シングルスレッドあるいはリニアオブジェクトという、同時にアクセスできるのが単一のスレッドだけとなるようにすればいいという考え方でした(ここの解説では、複数のリーダーを許すよう拡張してありますが)。こうした構造を用いると、同一のオブジェクトを使用するスレッドの数をより細かく制御・追跡できるようになりますし、該当オブジェクトが不要になった段階で知りたいのはこうした情報なのです。

著者紹介

Greg Travis(Greg Travis)
ニューヨーク在住のJavaプログラマ兼テクノロジーライター。ハイエンドPCゲーム業界で3年間を過ごした後に、EarthWebに参加し、当時最新鋭のJavaプログラミング言語を用いた新規テクノロジーを各種開発。1997年以降は、さまざまなWebテクノロジーについてのコンサルタントを務める。
関連テーマ
最新トップニュース
  • サイジニア株式会社は2008年10月8日、ANA セールス株式会社が運営する「ANA SKY WEB TOUR」のユーザー向けの旅行商品推奨システムとして、コミュニティディスカバリーエンジン「デクワス」を納入、稼働を開始したと発表した。
  • 国内日立電線が100ME「Apresia」2製品を発表(Webテクノロジー 10月8日 17:50)
    日立電線は2008年10月8日、イーサネットスイッチ「Apresia シリーズ」の100メガビットイーサネットスイッチ2製品を開発し、11月28日から販売を開始すると発表した。
  • ヤフー株式会社は2008年10月8日、同社の提供する有料会員サービス「Yahoo! プレミアム」にて、12月1日より会員費を月額294円(総額)から月額346円(総額)へ改定する、と発表した。
  • 国内DNP、多機能 IC カードの新タイプを開発(Webテクノロジー 10月8日 17:20)
    大日本印刷株式会社(DNP)は2008年10月8日、Java Card 版 FeliCa デュアルインターフェイスカードの新タイプを開発し、金融機関向けに10月中旬より販売を開始すると発表した。
  • 株式会社サンゼロミニッツは2008年10月8日、同社が運営するタウン情報検索サイト「30min.」のランチマップ機能が、米国時間10月7日に公開された Firefox3 のアドオン「Geode」に対応した、と発表した。
Graphic Design Forum
【Graphic Design Forum】
活気に満ちた誕生日をどうぞ (10月8日)
データメーション
【データメーション】
Google 版酒気検知機能が間もなく登場(10月8日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「ITを活用し、消費生活における意思決定の支援、悩み・迷いを解決する!」/株式会社ALBERT(10月8日)
エンジニアの独り言
【エンジニアの独り言】
得体の知れない情報(?)との向き合い方(9月17日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
昆虫と退屈なことについて(9月16日)
気になるトレンド用語
気になるトレンド用語
はてなブックマークが変わる!そもそもブラウザのお気に入りと何が違うの?(10月8日)
e-Japan 先端テクノロジー解説
e-Japan 先端テクノロジー解説
行政サービスのマルチチャネル化について(10月8日)
ウチのサイトを SEO
ウチのサイトを SEO
ちゃんと title つけていますか?(10月8日)
百式のネットビジネス研究
百式のネットビジネス研究
Blog 記事の編集を読者に任せることができる「gooseGrade」(10月8日)
「IT の耳」
「IT の耳」
【書評】ニコ動から RMT まで〜『人はなぜ形のないものを買うのか―仮想世界のビジネスモデル』(10月7日)
DevX
DevX
アジャイルソフトウェアプロジェクトを管理する(10月7日)
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
SEって、デジタル製品は判官びいきで選ぶよね?(10月7日)
アイレップの SEM フロンティア
アイレップの SEM フロンティア
フル CSS でサイトを構築する SEO のメリット(10月7日)
モバイルSEO@フラクタリスト
モバイルSEO@フラクタリスト
応用的な SEO 施策(3)(10月6日)
サーチからはじまるインタラクティブエージェンシー
サーチからはじまるインタラクティブエージェンシー
DB マーケティングと Web マーケティング 〜ビールとオムツの伝説から〜(10月6日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/