Javaコードの診断: 「みなし子スレッド」バグ・パターン

マスター・スレッドが自滅し、その他のスレッドが生き残っていると、どうなるか?

マルチスレッド・コードでは、単一のマスター・スレッドを使って、その他のスレッドの動作を制御する手法がよく使用されます。そのマスター・スレッドから、キューに入れるなどによりメッセージを送信して、他のスレッドでそれらを処理するということがよくあります。しかし、マスター・スレッドが例外をスローすると、残りのスレッドがキューへの入力を待機したまま実行を継続することになり、プログラムがフリーズしてしまうかもしれません。今回の「Javaコードの診断」では、フルタイムのJavaデベロッパーでありパートタイムのバグ駆除者であるEric Allen氏が、この手のバグ・パターンを検出、修正、および回避する方法について説明します。

Eric Allen (eallen@cs.rice.edu), Ph.D. candidate, Java programming languages team, Rice University

Eric Allen氏は、コーネル大学でコンピューター・サイエンスおよび数学を専攻し、A.B.(Bachelor of Arts)を取得しました、また現在は、ライス大学、Javaプログラミング言語チームの博士課程に在籍しています。彼の研究対象は、Java言語のソース / バイトコード・レベルでのセマンティック・モデルおよびスタティック分析ツールの開発です。また現在は、NextGenプログラミング言語 (ジェネリック・ランタイム・タイプを持つJavaの拡張言語) のためのソース・ツー・バイトコード・コンパイラーを作成しています。彼の連絡先は、eallen@cs.rice.edu です。



2001年 8月 01日

マルチスレッド・コーディングには、プログラマーにとってさまざまな利点があります。マルチスレッドにより、プログラミング作業 (およびプログラム) がずっと高速になり、リソースを非常に効率的に使用するコードを作成できます。しかし、どんなことでもそうですが、それには欠点があります。マルチスレッド・コードは本質的に非決定論的なところがあるため、エラーの発生する可能性はより大きくなります。さらに、発生するエラーは再現が非常に困難であり、そのために突き止めることが難しくなります。

みなし子スレッドのパターン

Javaプログラミング言語には、マルチスレッド・コードのためのサポートが豊富に用意されています。その中に、特に便利な機能が1つ含まれています。それは、他のスレッドに影響を及ぼすことなく、1つのスレッドで例外をスローする機能です。しかし、この機能のために、追跡困難なたくさんのバグが発生することがあります。

コードへ直行

リスト1
スレッドが互いに頻繁に通信するプログラムの例。
リスト2
例外をキャッチし、終了前に従属スレッドに対して問題発生を通知する方法を示す例。

複数のスレッドのうちの1つでクラッシュから回復するということに意味があるなら、この機能を使うことによってプログラムの頑強性が増し加わることがあります。しかし、そうすると、それらのスレッドの1つがいつ例外をスローしたかを判別することが困難になりかねません。残りのスレッドは実行を継続しているため、プログラムは、応答しないプログラムやフリーズしたプログラムの兆候を示すようになるかもしれません。スレッドが互いに頻繁に通信するプログラムでは、特にそう言えます。

リスト1に示す例をご覧ください。ここでは、2つのスレッドがプロデューサー/コンシューマー・モデルに従って通信しています。

リスト1. 簡単なコンシューマー/プロデューサー型マルチスレッド・プログラム
public class Server extends Thread {
  Client client;
  int counter;
  public Server(Client _client) {
    this.client = _client;
    this.counter = 0;
  }
  public void run() {
    while (counter < 10) {
      this.client.queue.addElement(new Integer(counter));
      counter++;
    }
    throw new RuntimeException("counter >= 10");
  }
  public static void main(String[] args) {
    Client c = new Client();
    Server s = new Server(c);
    c.start();
    s.start();
  }
}
class Client extends Thread {
  Vector queue;
  public Client() {
    this.queue = new Vector();
  }
  public void run() {
    while (true) {
      if (! (queue.size() == 0)) {
        processNextElement();
      }
    }
  }
  private void processNextElement() {
    Object next = queue.elementAt(0);
    queue.removeElementAt(0);
    System.out.println(next);
  }    
}

このような場合、第2のスレッドは、処理するデータを受け取ることに関して第1のスレッドに完全に依存しています。したがって、第1のスレッドがクラッシュすると (この例では確実にそうなる)、第2のスレッドは、もう決して来ることのない次の入力を待機したままになってしまいます。このようなわけで、私はこのバグ・パターンを「みなし子スレッド」パターンと呼んでいます。


症状

このバグ・パターンの症状として最もよくあるのは、前述のようにプログラムがフリーズしてしまうことです。

その他の症状としては、実際にはプログラムが停止した状態で、スタック・トレースが標準エラー出力と標準出力に出力されることがあります。


治療法と予防策

このバグ・パターンがあることがわかれば、クラッシュの発生しているスレッドに潜んでいるエラーを見つけて修正することが治療法となることは明らかです。しかし、予防はそれほど簡単ではありません。

言うまでもなく、単一スレッドに基づく設計で済むのであれば、それによって多くの頭痛の種がなくなります。しかし、プログラムのパフォーマンスに関する要件のために、当初からマルチスレッド設計が考慮されているということも十分考えられます。

この種のクラッシュを診断するのに役立つ1つの方法は、さまざまなスレッドでスローされた例外をキャッチし、終了前に従属スレッドに対して問題発生を通知する、ということです。そのようにしたのがリスト2です。

リスト2. クライアント・スレッドにエラーが通知される例
import java.util.Vector;
public class Server2 extends Thread {
  Client2 client;
  int counter;
  public Server2(Client2 _client) {
    this.client = _client;
    this.counter = 0;
  }
  public void run() {
    try {
      while (counter < 10) {
           this.client.queue.addElement(new Integer(counter));
           counter++;
      }
      throw new RuntimeException("counter >= 10");
    }
    catch (Exception e) {
	this.client.interruptFlag = true;
	throw new RuntimeException(e.toString());
    }
  }
  public static void main(String[] args) {
    Client2 c = new Client2();
    Server2 s = new Server2(c);
    c.start();
    s.start();
  }
}
class Client2 extends Thread {
  Vector queue;
    boolean interruptFlag;
  public Client2() {
    this.queue = new Vector();
    this.interruptFlag = false;
  }
  public void run() {
      while (! interruptFlag) { if (! (queue.size() == 0)) {
        processNextElement();
      }
    }
    // Processes whatever elements remain on the queue before exiting.
    while (! (queue.size() == 0)) {
      processNextElement();
    }
    System.out.flush();
  }
  private void processNextElement() {
    Object next = queue.elementAt(0);
    queue.removeElementAt(0);
    System.out.println(next);
  }    
}

スローされた例外を扱うには、System.exit を呼び出す方法もあります。プログラムのメイン・スレッドでクラッシュが発生し、その他のスレッドではクリティカル・リソースを管理しない場合には、この方法を使用できます。それ以外の場合、この方法ではちょっと危険です。たとえば、その他のスレッドのうちの1つがオープン・ファイルを管理している場合を考えてみましょう。その場合、プログラムを単純に終了すると、リソースのリークが発生する可能性があります。

上記の単純な例でさえ、サーバー・スレッドでSystem.exit を呼び出すと、キュー上に残っている要素を処理せずにクライアントが終了することになる可能性があります。

実際、このような問題があるためにSunでは、スレッドに対してstop を使わないようにと呼びかけています。きちんと処理しないままスレッドを停止すると、リソースが矛盾した状態のままになる可能性があるため、stop メソッドは、言語のセキュリティー・モデルに反しています。

Sunがそれを使わないようにと呼びかけている理由については、参考文献をご覧ください。


まとめ

今週のバグ・パターンをまとめると、次のようになります。

  • パターン: みなし子スレッド
  • 症状: マルチスレッド・プログラムがロックしてしまう。スタック・トレースが標準エラー出力にされる場合と出力されない場合がある。
  • 原因: いくつかのプログラム・スレッドが1つのスレッドからの入力を待機しているが、そのスレッドからスローされた例外が受け取られないまま、そのスレッドが終了してしまう。
  • 治療法と予防策: メイン・スレッドの中に例外処理コードを記述し、クラッシュが発生しそうになったなら、従属スレッドに対してそのことを通知する。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219619
ArticleTitle=Javaコードの診断: 「みなし子スレッド」バグ・パターン
publish-date=08012001