目次


リアルタイム Java、第 3 回: スレッド化と同期

Real-time Specification for Java におけるスレッド化と同期についての考慮事項

Comments

JLS (Java Language Specification) には、スレッド化と同期は Java プログラミング言語の中核をなす機能であると記述されています。このJLS の中核的な機能を何通りもの方法で拡張するのが、RTSJ です (JLS および RTSJ のリンクは「参考文献」に記載されています)。例えば、RTSJ では正規 Java スレッドよりも厳密なスケジューリング・ポリシーに従う新たなリアルタイム・スレッドのタイプを導入しています。また、優先度の継承も機能拡張の一例です。これは、ロックが競合した場合にどのようにロックの同期を管理するかを定義するロック・ポリシーです。

優先順位と優先キューの管理を理解すれば、スレッド化と同期に対する RTSJ の変更内容を理解しやすくなります。また、優先順位はRT アプリケーションが使用する重要なツールでもあります。この記事では、スレッドの優先度と優先キューの管理方法に焦点を当て、RTSJのスレッド化と同期の特徴を説明します。さらに、IBM WebSphere® Real Time (「参考文献」を参照) を使用してビルドした RT アプリケーションをはじめとする、RT アプリケーションを開発、デプロイ、そして実行する際に考慮に入れなければならない事項も取り上げます。

正規 Java スレッドについて

JLS で定義されているスレッドは、正規 Java スレッドと呼ばれます。正規 Java スレッドは java.lang.Thread クラスのインスタンスで、1 から 10 までの整数値の優先度を持ちます。いくつもの実行プラットフォームに対応するため、JLS では正規 Javaスレッドの実装、スケジューリング、そしてその優先順位の管理に極めて高い柔軟性を持たせています。

WebSphere Real Time を含め、Linux® 上の WebSphere VM は Linux オペレーティング・システムに用意されたネイティブのスレッド化サービスを使用します。つまり、Linuxのスレッド化と同期の特徴を理解すれば、Java のスレッド化と同期の特徴も理解できるというわけです。

Linux のスレッド化と同期

Linux オペレーティング・システムはその歴史を通して、さまざまなユーザー・レベルのスレッド化の実装を提供してきました。Linux での戦略的スレッド化における最新の動向は、WebSphereVM でも使用されている NPTL (Native POSIX Thread Library) (「参考文献」を参照) です。今までのスレッド化に勝る NPTL の利点は、POSIX に準拠していること、そして優れたパフォーマンスです。POSIX サービスはコンパイル時にはシステムのヘッダー・ファイルによって、そして実行時にはlibpthread.so 動的ライブラリーと基礎となる Linux カーネル・サポートを通じて利用可能になります。Linux カーネルは、静的制御 (スレッドの優先レベルなど)とシステムで実行されているスレッドの特定の動的条件に基づいてスレッドをスケジューリングします。POSIX では、POSIX スレッド (pthread)を作成する際に、アプリケーションのニーズに応じたスケジューリング・ポリシーと優先度をスレッドに設定することが可能です。スケジューリング・ポリシーには、以下の3 つがあります。

  • SCHED_OTHER
  • SCHED_FIFO
  • SCHED_RR

SCHED_OTHER ポリシーは、プログラム開発ツールやオフィス・アプリケーション、そして Web ブラウザーといった通常のユーザー・タスク用です。一方、SCHED_RRSCHED_FIFO は、確定性と時間的な要求がもっと厳しいアプリケーションを対象に使用されます。SCHED_RRSCHED_FIFO との主な違いは、SCHED_RR はタイムスライス方式でスレッドを実行する一方、SCHED_FIFO はタイムスライス方式を使わないという点です。以下に、WebSphere Real Time で使用する SCHED_OTHERSCHED_FIFO ポリシーについて詳しく説明します (SCHED_RR ポリシーは WebSphere Real Time では使用されないので説明を省きます)。

POSIX がロックと同期をサポートするために使用するのは、pthread_mutex データ型です。pthread_mutex は、異なるロック・ポリシーを使用して作成できます。ロック・ポリシーの目的は、複数のスレッドが同じロックを同時に取得しようとした場合の動作を制御することです。標準Linux バージョンがサポートするのは単一のデフォルト・ポリシーだけですが、RT Linux バージョンは優先度の継承によるロック・ポリシーもサポートします。この優先度の継承ポリシーについては、「同期の概要」セクションで詳しく説明します。

Linux のスケジューリングとロックには、ファーストイン・ファーストアウト (FIFO) キューの管理が伴います。

正規 Java スレッドでのスレッド・スケジューリング

RTSJ では、正規 Java スレッドの動作は JLS での定義に従うことになっています。WebSphere Real Time での正規Java スレッドは、Linux の POSIX SCHED_OTHER スケジューリング・ポリシーを使用して実装されます。SCHED_OTHER ポリシーが対象とするのはコンパイラーやワード・プロセッサーなどのアプリケーションで、確定性が求められるタスクは対象外です。

2.6 Linux カーネルでは、SCHED_OTHER ポリシーは 40 の優先レベルをサポートします。この 40 の優先レベルはプロセッサーごとに管理されますが、それは以下のことを意味します。

  • Linux は、キャッシュのパフォーマンス向上のために、1 つのスレッドを 1 つのプロセッサー上で実行しようとします。
  • 必要であれば、Linux がプロセッサー間でスレッドをマイグレーションしてワークロードのバランスをとります。

40 ある優先レベルのそれぞれでは、Linux が active キューと expired キューを管理します。各キューにはスレッドのリンク・リストが含まれ、リンク・リストが含まれない場合は空のキューとなります。activeキューと expired キューは、効率性やロード・バランシング、そしてその他の目的で使用されます。論理的には、システムが 40 ある各優先度ごとに、ランキューと呼ばれる単一のFIFO キューを管理していると考えることができます。スレッドは、優先度が最も高く、空ではないランキューの先頭からディスパッチされます。このスレッドはキューから取り出されて、タイムクォンタム、あるいはタイムスライスと呼ばれる期間、実行されます。実行中のスレッドがタイムクォンタムを使い切ると、その優先度に対応したランキューの末尾に配置され、新しいタイムクォンタムが割り当てられます。同じ優先度の中では、キューの先頭にあるスレッドがディスパッチされ、タイムクォンタムを使い切ったスレッドはキューの末尾に配置されるというように、ラウンドロビン方式でスレッドが実行されます。

スレッドに指定されるタイムクォンタムの長さは、そのスレッドに割り当てられた優先度によって決まります。割り当てられた優先度が高いほど、指定されるタイムクォンタムも長くなります。スレッドがCPU を占有しないようにするため、Linux はスレッドが I/O バウンドか、あるいは CPU バウンドかどうかなどの要素に基づいてスレッドの優先順位を動的に上げたり下げたりします。スレッド自体が実行権を放棄すること(Thread.yield() の呼び出しなど) によってタイムスライスを放棄したり、ブロックによって制御を放棄したりすると、スレッドはイベント待ちの状態になります。待機の対象となるイベントの一例としては、ロックの解除によってトリガーされるイベントなどがあります。

WebSphere Real Time の VM は、SCHED_OTHER ポリシーの 40 ある Linux スレッドの優先順位の中で、正規 Java スレッドに 10 の優先度を明示的に割り当てることはしません。すべての正規Java スレッドには、その Java スレッドの優先度に関わらずデフォルトの Linux スレッドの優先度が割り当てられます。デフォルトのLinux スレッドの優先度は、40 ある SCHED_OTHER の優先順位のなかで中間に位置します。Linux が動的に優先順位を調整したとしても、最終的にはランキューに含まれるすべての正規 Java スレッドが実行されるという点で、デフォルトが割り当てられた正規Java スレッドは公正に実行されます。ここで前提となるのは正規 Java スレッドのみを実行するシステムなので、例えば RT スレッドを実行するシステムには当てはまりません。

WebSphere Real Time の VM、そして RT バージョン以外の WebSphere VM は、正規 Java スレッドに対して、SCHED_OTHER ポリシーとデフォルトの優先度を割り当てることに注意してください。同じポリシーを使用することで、両方の JVM に同様の、ただし同一ではないスレッド・スケジューリングおよび同期の特性を持たせています。ただし、両方のJVM のタイミングおよびパフォーマンス特性を同一にしてアプリケーションを実行することはできません。その理由は、RTSJ のサポートおよび RTMetronome ガーベッジ・コレクター (「参考文献」を参照) 導入のサポートをする上での WebSphere Real Time クラス・ライブラリーの変更、JVM の変更、そして JIT コンパイラーの変更によるものです。IBMWebSphere Real Time のテストでは、タイミングの差によって、他の JVM では何年も順調に実行されていたテスト・プログラムでのレース・コンディション(すなわち、バグ) が明るみに出ました。

正規 Java スレッドを使用したコードの例

リスト 1 に示す正規 Java スレッドを使用したプログラムは、作成される 2 つのスレッドのそれぞれが、5 秒間に何回ループの繰り返しを実行できるかを判断するためのものです。

リスト 1. 正規 Java スレッド
class myThreadClass extends java.lang.Thread
 {   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myThreadClass thread1 = new myThreadClass();
      thread1.setPriority(4);    // 1st thread at 4th non-RT priority
      myThreadClass thread2 = new myThreadClass();
      thread2.setPriority(6);    // 2nd thread at 6th non-RT priority
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

リスト 1 のプログラムで使用している正規 Java スレッドは、以下の 3 つのユーザー・スレッドです。

  • 基本スレッド:
    • これはメイン・スレッドで、プロセスの起動時に暗黙的に作成され、main() メソッドを実行します。
    • main() メソッドが作成する 2 つの正規 Java スレッドは、一方が優先度 4、もう一方が優先度 6 となります。
    • このメイン・スレッドは Thread.sleep() を呼び出して 5 秒間スリープすることで、スレッド自体を意図的にブロックします。
    • このスレッドは 5 秒間スリープした後、他の 2 つのスレッドに終了するよう指示します。
  • 優先度 4 のスレッド:
    • 基本スレッドによって作成されるスレッドで、for ループが含まれる run() メソッドを実行します。
    • このスレッドは、以下のアクションを実行します。
      1. ループの繰り返しごとにカウントをインクリメントします。
      2. Thread.yield() を呼び出してタイムスライスを自発的に放棄します。
      3. メイン・スレッドの要求に応じて終了します。終了する直前に、ループ・カウントを出力します。
  • 優先度 6 のスレッド: このスレッドが実行するアクションは、優先度 4 のスレッドと同じです。

上記のプログラムをユニプロセッサーまたはアンロードされたマルチプロセッサー・システムで実行すると、それぞれのスレッドが出力するループ繰り返しカウントはほとんど同じになります。1回の実行でプログラムが出力するのは以下の内容です。

 Created thread
 Created thread
 Thread terminates. Loop count is 540084
 Thread terminates. Loop count is 540083

Thread.yield() の呼び出しを削除すると、2 つのスレッドのループ・カウントは近くなりますが、同一になることはまずありません。両方のスレッドには、SCHED_OTHER ポリシーで同じデフォルトの優先度が割り当てられるので、指定されるタイムスライスも同じです。この 2 つのスレッドは同じコードを実行することから、同じような動的調整が優先順位に加えられ、同じランキューからラウンドロビン方式で実行されることになります。ただし、優先度4 のスレッドが最初に実行されるため、5 秒の実行期間内に占める割り当てがもう一方のスレッドよりわずかに大きくなります。したがって、優先度 4のスレッドが出力するループ・カウントのほうが大きくなります。

RT スレッドについて

RT スレッドとは、javax.realtime.RealtimeThread のインスタンスのことです。RTSJ では、仕様の実装が RT スレッドに対して少なくとも 28 の連続する値で優先度を提供することを要件としています。これらの優先度は、リアルタイム優先度と呼ばれます。仕様では、RT優先度が取りうる値について、正規 Java スレッドに指定される最高の優先度である 10 より大きな値にするということ以外に規定していません。移植性の理由から、アプリケーション・コードは新しいPriorityScheduler クラスの getPriorityMin() メソッドと getPriorityMax() メソッドを使って有効な RT優先度が取りうる値の範囲を決定することになっています。

RT スレッドが必要になった理由

JLS でのスレッド・スケジューリングはあいまいで、提供する優先度の値も 10 個しかありません。Linux が実装する POSIX の SCHED_OTHER ポリシーはさまざまなアプリケーションのニーズを満たしますが、この SCHED_OTHER ポリシーには望ましくない点があります。それは、優先順位の動的な調整とタイムスライスの割り当てが予測不可能な頻度で発生する可能性があることです。SCHED_OTHER の優先度の数 (40) は、それに対応できるほど多くはなく、しかもこの優先度の範囲は、正規 Java スレッドを実装したアプリケーションによって、動的に優先順位が調整されることがすでに前提となっています。さらに、JVMでもガーベッジ・コレクション (GC) などの特殊な目的を持つ内部スレッド用の優先度を必要とします。

確定性の欠如、より多くの優先レベルの必要性、そして既存のアプリケーションとの互換性の要望が、Java プログラマーに新しいスケジューリング機能を提供するための拡張が必要となった理由です。RTSJに記述されているように、javax.realtime パッケージのクラスはそのような機能を提供します。WebSphere Real Time では、Linux の SCHED_FIFO スケジューリング・ポリシーで RTSJ スケジューリングのニーズに対処します。

RT Java スレッドでのスレッド・スケジューリング

WebSphere Real Time では、11 から 38 までの合計 28 の RT Java 優先度がサポートされます。この優先度の範囲は、PriorityScheduler クラスの API を使用して取得されます。このセクションでは、RTSJ に記述されているスレッド・スケジューリング、そして RTSJ の要件以上のものをもたらすLinux の SCHED_FIFO ポリシーの特徴について詳しく説明します。

RTSJ では、RT 優先度を論理的に実装するのは、RT 優先度ごとの個別キューを維持するランタイム・システムであると考えています。スレッド・スケジューラーは、空ではないキューのうち、最高優先度のキューの先頭からスレッドをディスパッチしなければなりません。ここで注意しなければならないのは、RT優先度を持つどのキューの中にもスレッドがない場合、正規 Java スレッドがディスパッチされ、JLS での記述に従って実行されるという点です(「正規 Java スレッドによるスレッド・スケジューリング」を参照)。

ディスパッチされた RT 優先度を持つスレッドが実行を継続できるのは、このスレッドがブロックされるまで、あるいはこのスレッドが実行権の放棄によって自発的に制御を放棄するまで、もしくはこのスレッドより高いRT 優先度を持つスレッドによってプリエンプトされるまでです。RT 優先度を持つスレッドは自発的に実行権を放棄すると、その優先度のキューの末尾に配置されます。RTSJでは、このようなスケジューリングが実行中の RT スレッドの数などの要素によって影響されることなく定期的に行われることも要件としています。バージョン1.02 の RTSJ では、これらの規則をユニプロセッサー・システムに適用していますが、マルチプロセッサー・システムに関しては、スケジューリングがどのように行われるべきかを規定してはいません。

Linux は、RTSJ スケジューリング要件のすべてを SCHED_FIFO ポリシーによって実現します。SCHED_FIFO ポリシーの対象はユーザー・タスクではなく RT スレッドです。SCHED_FIFOSCHED_OTHER ポリシーと異なる点は、99 の優先レベルを提供すること、そしてスレッドにタイムスライスを設定しないことです。さらに、SCHED_FIFO ポリシーは、「同期の概要」セクションで説明する優先度の継承によるロック・ポリシーの調整以外では、RT スレッドの優先順位の動的な調整を行いません。優先度の継承があるため、RTSJでは優先順位の調整を要件としています。

Linux は RT スレッドと正規 Java スレッドの両方に対して常にタイム・スケジューリングを行います。マルチプロセッサー・システムでは、Linuxは使用可能なプロセッサーにディスパッチされた RT スレッドに対応する単一のグローバル・キューの動作をエミュレートしようとします。これは RTSJの精神に最も近いとはいえ、正規 Java スレッドに使用する SCHED_OTHER ポリシーとは異なります。

RT スレッドを使用した問題のあるコードの例

リスト 2 では、リスト 1 のコードが正規 Java スレッドではなく RT スレッドを作成するように変更しています。その違いを表しているのは、java.lang.Thread の代わりに java.realtime.RealtimeThread を使用しているところです。getPriorityMin() メソッドの決定に従って、最初のスレッドは 4 番目の RT 優先レベルで、2 つ目のスレッドは 6 番目の RT 優先レベルで作成されます。

リスト 2. RT スレッド
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 2nd thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

リスト 2 の変更されたコードには、いくつかの問題があります。このプログラムをユニプロセッサー環境で実行すると、プログラムは終了することはなく以下を出力するだけとなります。

Created thread

上記の結果は、RT スレッド・スケジューリングの動作によって説明できます。まず、基本スレッドは正規 Java スレッドのままなので、RT 以外のポリシー(SCHED_OTHER) で実行されます。そのため、基本スレッドが最初の RT スレッドを開始すると同時に、RT スレッドが基本スレッドをプリエンプトします。このRT スレッドは、タイムクォンタムで制限されることも、スレッドのブロックによって制限されることもないので無制限に実行されます。したがって、基本スレッドがプリエンプトされた後に基本スレッドの実行が許可されることはなく、2番目の RT スレッドも開始されないことになります。Thread.yield() では基本スレッドの実行を許可できません。実行権を放棄すると RT スレッドは必然的にそのランキューの末尾に配置されることになりますが、このスレッドが最高優先度のランキューの先頭となるので、スレッド・スケジューラーによって再びディスパッチされてしまいます。

このプログラムは 2 個のプロセッサーを備えたシステムでも失敗します。この場合には以下の内容が出力されます。

Created thread
Created thread

基本スレッドは RT スレッドを両方とも作成できますが、2 番目の RT スレッドを作成すると基本スレッドはプリエンプトされます。2 つの RTスレッドは 2 個のプロセッサーそれぞれで実行され、ブロックされる可能性がまったくないため、基本スレッドは RT スレッドに終了の指示を出せません。

プログラムの実行が完了すると、3 個またはそれ以上のプロセッサーを備えたシステムに関する結果が生成されます。

シングル・プロセッサーで実行する RT コードの例

リスト 3 に、ユニプロセッサー・システムで正しく実行されるように変更したコードを示します。main() メソッドのロジックは、8 番目の RT 優先度を持つ「メイン」の RT スレッドに移されています。この優先度はメイン RT スレッドが作成する他の2 つの RT スレッドに指定された優先度よりも高い優先度です。RT 優先度を最高位にすることで、このメイン RT スレッドは 2 つの RTスレッドを正常に作成できるだけでなく、メイン RT スレッドが 5 秒間のスリープ状態からウェイクアップしたときに、現在実行中のスレッドをプリエンプトできるようになります。

リスト 3. 変更された RT スレッドの例
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   static class myRealtimeStartup extends javax.realtime.RealtimeThread {

   public void run() {
      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 1st thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      try {
                        Thread.sleep(5*1000);
      } catch (InterruptedException e) {
      }
      myRealtimeThreadClass.Stop = true;
      }
   }

   // Primordial thread creates real-time startup thread
   public static void main(String args[]) {
      myRealtimeStartup startThr = new myRealtimeStartup();
      startThr.setPriority(PriorityScheduler.getMinPriority(null)+ 8);
      startThr.start();
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

このプログラムをユニプロセッサーで実行すると、以下の内容が出力されます。

Created thread
Thread terminates. Loop count is 32767955
Created thread
Thread terminates. Loop count is 0

プログラムの出力には、すべてのスレッドが実行、終了されたことが示されていますが、2 つのスレッドのうち、for ループの繰り返しを実行しているのは 1 つだけです。このような出力になる理由は、RT スレッドの優先度を考えると説明がつきます。まず、メインRT スレッドが、Thread.sleep() の呼び出しでスレッドがブロックされるまで実行されます。メイン RT スレッドは 2 つのスレッドを作成しますが、メイン RT スレッドのスリープ中に実行を許可されるのは6 番目の RT 優先度を持つ 2 つ目の RT スレッドだけです。このスレッドは、スリープ状態からウェイクアップしたメイン RT スレッドによって終了が指示されるまで実行されます。メインRT スレッドが終了すると 6 番目の優先度を持つスレッドが実行、終了できるようになり、このスレッドがゼロ以外の値のループ・カウントを出力します。6番目の優先度を持つスレッドが終了すると、4 番目の優先度を持つスレッドが実行可能になりますが、すでに終了するよう指示されているので for ループをバイパスし、終了する前にゼロのループ・カウントを出力するというわけです。

RT アプリケーションでのスレッド化に関する考慮事項

このセクションでは、RT スレッドを使用するようにアプリケーションを移植する際、または RT スレッド化を活用するアプリケーションを新規に作成する際に考慮すべきRT スレッド化の特徴について説明します。

RT スレッドの新規拡張機能

RTSJ は、特定の時刻または相対的な時間に開始する RT スレッドを作成する機能を仕様に定めています。例えば、指定した時間間隔または期間で特定のロジックを実行するスレッドを作成し、このロジックが指定した時間内に完了しなかった場合にはAsynchronousEventHandler(AEH) を実行 (起動) するように定義することができます。さらに、スレッドが使用できるメモリーのタイプや量に制限を定義し、スレッドがその制限を超えてメモリーを使用した場合にOutOfMemoryError をスローすることもできます。これらの機能は RT スレッドのみを対象としているため、正規 Java スレッドには使用できません。それぞれの機能についての詳細は、RTSJを参照してください。

Thread.interrupt() とペンディング例外

Thread.interrupt() 動作は RT スレッド用に拡張されています。この API は、JLS の記述に従ってブロックされたスレッドに対して割り込みを行います。この例外は、ユーザーがメソッド宣言にThrows AsynchronouslyInterruptedException 節を追加して明示的に割り込み可能のマークを付けたメソッドでも発生します。また、ユーザーがこの例外を明示的にクリアしなければスレッドにバインドされたままになってしまう(ペンディングと呼ばれます) という点でも、スレッドにとって厄介な例外です。ユーザーがこの例外をクリアしないと、スレッドはこの厄介な例外がバインドされたまま終了する場合があります。このエラーは、スレッドが「通常」の方法で終了すれば害になりませんが、独自の形式でRT スレッドのプーリングを行うアプリケーションの場合はその限りではありません。InterruptedException がバインドされたままのスレッドがプールに戻されてしまうからです。スレッド・プーリングを実行するコードが明示的に例外をクリアしないと、例外がバインドされたままプールに入れられたスレッドが再び割り当てられ、この例外が誤ってスローされてしまう可能性があります。

基本スレッドとアプリケーションのディスパッチ・ロジック

基本スレッドは常に RT スレッドではなく正規 Java スレッドなので、最初の RT スレッドは常に正規 Java スレッドによって作成されます。使用可能なプロセッサーが不足していてRT スレッドと正規 Java スレッドを同時に実行できない場合、作成された RT スレッドが直ちに、正規 Java スレッドをプリエンプトします。プリエンプションによって、正規Java スレッドは、さらなる RT スレッドやその他のロジックを作成できなくなるため、アプリケーションを適切に初期化された状態にすることはできません。

この問題は、優先度の高い RT スレッドからアプリケーション初期化を実行することによって回避できます。この手法は、独自の形式でスレッド・プーリングやスレッド・ディスパッチを行うアプリケーションまたはライブラリーで必要になる場合があります。つまりスレッド・ディスパッチ・ロジックは、高い優先度で実行するか、あるいは高い優先度のスレッド内で実行しなければなりません。スレッド・プーリング・ロジックを実行するのに適切な優先度を選択すれば、スレッドのエンキューやデキューで発生する問題の防止に役立ちます。

ランナウェイ・スレッド

正規 Java スレッドはタイムクォンタムで実行し、スケジューラーが CPU 使用時間に基づいて動的に優先順位を調整するので、最終的にはすべての正規Java スレッドが実行されることになります。それとは逆に、RT スレッドはタイムクォンタムに制約されず、スレッド・スケジューラーは CPU使用時間に基づいて動的な優先順位の調整を行いません。正規 Java スレッドと RT スレッドとの間のこのようなスケジューリング・ポリシーの違いは、ランナウェイRT スレッドが発生する可能性を生み出します。ランナウェイ RT スレッドはシステムの制御権を握り、他のアプリケーションを実行不可能にしたり、ユーザーがシステムにサインオンできないようにしたりします。

開発とテストの段階でランナウェイ・スレッドによる影響を抑えるために役立つ手法は、プロセスが使用する CPU の量に制限を設けることです。Linuxでは、CPU 使用時間を制限すると、その制限時間を使い果たしたランナウェイ・スレッドがキルされます。また、システム状態を監視するプログラムやシステム・ログインを提供するプログラムは高いRT 優先度で実行し、問題のあるスレッドをプリエンプトできるようにしてください。

Java 優先順位とオペレーティング・システム優先順位のマッピング

Linux では、POSIX SCHED_FIFO ポリシーが整数値 1 から 99 までの合計 99 の RT 優先度を提供します。このシステム範囲のうち、WebSphere VM が使用するのは11 から 89 までの優先度で、この範囲のサブセットを使用して 28 の RTSJ 優先度を実装します。28 の RT Java 優先度をこのPOSIX システムの優先順位にマッピングする方法は、IBM WebSphere Real Time の資料で説明しています。ただしアプリケーション・コードについては、このマッピングに依存せず、Javaレベルで相対的に順序付けられた 28 の RT 優先順位だけに依存するようにしてください。それにより、JVM でこの優先順位の範囲を再マッピングし、今後のWebSphere Real Time のリリースで改善できるようになります。

アプリケーションに WebSphere Real Time で使用されている優先度より上または下の RT 優先度が必要な場合、SCHED_FIFO の優先度 1 または 90 を使用してデーモンやその他の RT プロセスを実装することができます。

JNI の AttachThread()

JNI (Java Native Interface) では、JNI の AttachThread() API を使用して C コードで作成されたスレッドを JVM にアタッチできるようになっていますが、RTSJ では RT スレッドをアタッチするためのJNI インターフェースの変更または設定を行っていません。したがって、アプリケーションでは JVM にアタッチすることを目的とした C コードのPOSIX RT スレッドを作成しないようにしてください。そのような RT スレッドは Java 言語で作成する必要があります。

フォークされたプロセスと RT 優先度

スレッドは別のプロセスをフォークすることができます。Linux では、フォークされたプロセスの基本スレッドには、それをフォークした親スレッドの優先度が継承されます。フォークされたプロセスがJVM の場合、JVM の基本スレッドは RT 優先度で作成されます。そうなると、正規 Java スレッド (基本スレッドなど) は RT スレッドより低いスケジューリング優先度を持つという序列に違反することになります。このような事態を防ぐため、JVMは基本スレッドに RT 以外の優先度、つまり SCHED_OTHER ポリシーを持たせています。

Thread.yield()

Thread.yield() によって実行権を放棄して譲渡する対象は同じ優先度のスレッドに対してだけで、それより上や下の優先度を持つスレッドに実行権を譲渡することは決してありません。同じ優先度のスレッドのみに譲渡するということは、複数のRT 優先度を使用する RT アプリケーションでは Thread.yield() が問題になるということです。絶対的に必要でない限り、Thread.yield() は使用しないようにしてください。

NoHeapRealtimeThread

RTSJ の新しいスレッド・タイプとしては、javax.realtime.NoHeapRealtimeThread (NHRT) もあります。これは、javax.realtime.RealtimeThread のサブクラスです。NHRT は基本的に、今まで説明した RT スレッドと同じスケジューリング特性を持ちますが、NHRT は GC によってプリエンプトされないこと、そしてNHRT は Java ヒープに対する読み取り書き込みができないという点が異なります。NHRT は RTSJ の重要な特徴なので、この連載の今後の記事で取り上げる予定です。

AsynchronousEventHandler

RTSJ で新たに登場した AsynchronousEventHandler (AEH) は、イベントが発生すると実行されるという RT スレッドの 1 つの形として見なすことができます。例えば、AEH は特定の時刻または相対的な時間経過後に起動するように設定できます。AEHには RT スレッドと同じスケジューリング特性があり、ヒープ内に置くこともヒープには置かないことも可能です。

同期の概要

多くの Java アプリケーションは Java スレッド化機能を直接使用します。あるいは開発中のアプリケーションが複数のスレッドを必要とするライブラリーを使用するという場合もあります。マルチスレッド化プログラムにおける第一の関心事は、複数のスレッドが実行されるシステムで、プログラムが正しく(スレッド・セーフに) 実行されるようにすることです。プログラムをスレッド・セーフにするには、複数のスレッドが共有するデータへのアクセスを、ロックやアトミック・マシン操作などの同期プリミティブによって逐次化しなければならない場合があります。RTアプリケーションのプログラマーが、特定の時間制約のなかでプログラムが実行されるようにするという課題を抱えることも珍しくありません。この課題に対処するには、使用するコンポーネントの実装詳細、包含関係、そしてパフォーマンス属性を知る必要があります。

この記事の残りでは、Java 言語が提供する中核的な同期プリミティブの特徴、これらのプリミティブの RTSJ での変更内容、そして RT プログラマーがプリミティブを使用する際に認識しておかなければならないそのいくつかの事項について説明します。

Java 言語同期の概要

Java 言語は、以下の 3 つの中核的同期プリミティブを提供します。

  • 同期化されたメソッドとブロックにより、スレッドは (メソッドまたはブロックに対して) 開始時にオブジェクトをロックし、終了時にロック解除することができます。
  • Object.wait() がオブジェクトのロックを解除すると、スレッドが待機状態になります。
  • Object.wait()Object.wait() によりオブジェクト上で待機状態になっているスレッドをブロック解除します。Object.wait() は待機中のすべてのスレッドをブロック解除します。

wait() および notify() を実行するスレッドは、オブジェクトのロックを保持しているスレッドでなければなりません。

あるスレッドによってロック済みのオブジェクトを別のスレッドがロックしようとすると、ロック競合が発生します。この場合、ロックの取得に失敗したスレッドは、そのオブジェクトに対するロック候補の論理キューに入れられます。同様に、複数のスレッドが同じオブジェクトでObject.wait() を実行する可能性があるため、そのオブジェクトに対する待機スレッドの論理キューもあります。JLS ではこれらのキューの管理方法を指定していませんが、RTSJではキューの動作を規定しています。

優先度ベースの同期キュー

RTSJ の精神は、スレッドのすべてのキューは FIFO であり、優先度に基づくというものです。優先度ベースの FIFO 動作 (前に記載した同期の例でのように、最高優先度のスレッドが次の実行対象として選択される)は、ロック候補キューおよびロック待機キューにも適用されます。論理的視点から見ると、ロック候補のスレッドには、実行を待機中のスレッドの実行キューと同じようなFIFO 優先度ベースのキューがあります。また、ロックを待機するスレッドにも同様のキューがあります。

ロックが解除されると、システムはロック候補の最高優先キューの先頭にあるスレッドを選択して、オブジェクトをロックしようとします。同様に、notify() が実行されると、待機スレッドの最高優先キューの先頭にあるスレッドの待機状態がブロック解除されます。ロック解除またはロックの notify() 操作は、最高優先キューの先頭にあるスレッドに適用されるという点で、スケジューリングのディスパッチ操作と似ています。

優先度ベースの同期をサポートするため、RT Linux には変更が必要でした。また、WebSphere Real Time の VM にも、notify() 操作の実行時にブロック解除の対象スレッドを選択する役目を Linux に委任するための変更が必要でした。

優先順位の逆転と優先度の継承

優先順位の逆転とは、低い優先度のスレッドが保持するロックで、高い優先度のスレッドがブロックされてしまうという状態です。低い優先度のスレッドがロックを保持している間に、中間の優先度のスレッドが低い優先度のスレッドをプリエンプトし、低い優先度のスレッドよりも優先して実行されることもあります。優先順位の逆転は、低い優先度のスレッドと高い優先度のスレッドの両方の進行を遅らせます。優先順位の逆転による遅延が原因で、重要な期限に間に合わなくなることも考えられます。この状況を示しているのが、図1 の最初の時系列です。

優先度の継承は、優先順位の逆転を回避するための手法です。RTSJ では、優先度の継承を要件としています。優先度の継承の背後にある概念は、ロック競合の時点で、ロックを保持するスレッドの優先度をそのロックを取得しようとしているスレッドの優先度まで格上げするというものです。ロックを保持するスレッドの優先度は、スレッドがロックを解除すると元の優先度に「格下げ」されます。上記で説明したシナリオでは、ロック競合が発生すると、低い優先度のスレッドはロックを解除する時点まで高い優先度で実行されます。ロックが解除されると、高い優先度のスレッドがオブジェクトをロックして実行を続けます。そのため、高い優先度のスレッドによって高い優先度のスレッドに遅延が生じることはありません。図1 の 2 番目の時系列に、優先度の継承が有効になると最初の時系列のロック動作がどのように変更されるかを示します。

図 1. 優先順位の逆転と優先度の継承
優先順位の逆転と優先度の継承
優先順位の逆転と優先度の継承

高い優先度のスレッドが低い優先度のスレッドのロックを取得しようとするのと同時に、低い優先度のスレッド自体が別のスレッドが保持するロックでブロックされることもあり得ます。このような場合は、低い優先度のスレッドとその別のスレッドの両方が格上げされます。つまり、優先度の継承には、スレッド・グループの優先度の格上げ、格下げが必要になる場合もあるということです。

優先度の継承の実装

優先度の継承は Linux カーネル機能によって行われます。この機能は POSIX ロック・サービスによってユーザー空間にエクスポートされますが、以下の理由により、ユーザー空間内だけでのソリューションは望ましくありません。

  • Linux カーネルがプリエンプトされ、優先順位の逆転が発生する場合があるため。特定のシステム・ロックには、優先度の継承も必要になります。
  • ユーザー空間でソリューションを実行しようとすると、解決するのが難しい競合状態が発生するため。
  • 優先度の格上げには、いずれにしてもカーネルの呼び出しが必要なため。

POSIX のロック・タイプは pthread_mutex です。pthread_mutex を作成するための POSIX API では、ミューテックスに優先度の継承プロトコルを実装させることができます。POSIX には、pthread_mutex をロックするためのサービス、pthread_mutex をロック解除するためのサービスがあり、この 2 つのサービスで優先度の継承サポートは有効になります。Linux は、ロック競合がなければ、すべてのロックをユーザー空間で行います。ロック競合が発生すると、優先度の格上げと同期キューの管理がカーネル空間で行われます。

WebSphere VM は POSIX のロック API を使用して前述の Java 言語の中核的同期プリミティブを実装し、優先度の継承をサポートします。ユーザー・レベルのC コードでこれらの POSIX サービスを使用することも可能です。Java レベルでのロック操作の時点で、固有の pthread_mutex が割り当てられ、アトミック・マシン操作で Java オブジェクトにバインドされます。Java レベルでのロック解除操作では、ロックの競合がなければアトミック操作によってpthread_mutex がオブジェクトからアンバインドされます。競合が発生した場合は、POSIX のロックおよびロック解除操作が Linux カーネルの優先度の継承サポートをトリガーすることになります。

ミューテックスの割り当ておよびロック時間が最小限になるように、JVM はグローバル・ロック・キャッシュとスレッド単位のロック・キャッシュを管理します。それぞれのキャッシュに含まれるのは、未割り当てのpthread_mutex です。スレッド固有のキャッシュのミューテックスは、グローバル・ロック・キャッシュから取得されます。ミューテックスはスレッド・ロック・キャッシュに配置される前に、スレッドによって事前にロックされます。非競合のロック解除操作では、ロックされたミューテックスをスレッド・ロック・キャッシュに戻します。ここで前提とするのは、非競合のロック操作を基準とし、事前にロックされたミューテックスを再使用することによってPOSIX レベルのロックを減らすとともに解消するということです。

JVM 自体が持つ内部ロックは、スレッド・リストやグローバル・ロック・キャッシュなどの重要な JVM リソースへのアクセスを逐次化するために使用されます。これらの内部ロックは優先度の継承に基づき、保持されるのは短期間です。

RT アプリケーションでの同期に関する考慮事項

このセクションでは RT 同期の特徴をいくつか説明します。開発者がこれらの特徴を知っておくと、RT スレッドを使用するためにアプリケーションを移植する際、あるいはRT スレッド化を利用するアプリケーションを新規に作成する際に役立ちます。

正規 Java スレッドと RT スレッドとのロック競合

RT スレッドは、正規 Java スレッドが保持するロックでブロックされる場合があります。このような事態は優先度の継承に引き継がれるため、正規Java スレッドがロックを保持する限り、その優先度は RT スレッドの優先度まで格上げされます。この場合、正規 Java スレッドは以下のように、RTスレッドのスケジューリング特性をすべて継承します。

  • 正規 Java スレッドは SCHED_FIFO ポリシーで実行されるため、タイムスライスを実行しません。
  • ディスパッチと実行権の放棄は、優先度が格上げされた RT ランキューから行われます。

上記の動作は、正規 Java スレッドがロックを解除すると SCHED_OTHER に戻ります。RT スレッドが必要とするロックが正規 Java スレッドによって保持されている間にリスト 1 で作成したスレッドのいずれかが実行されることになっていた場合、そのプログラムは終了せず、「RT スレッドを使用した問題のあるコードの例」で説明した問題が起こります。このような事態が考えられるため、リアルタイム JVM 内で実行するスレッドではスピン・ループと実行権の放棄を行わないようにしてください。

NHRT と RT スレッドとのロック競合

NHRT は、RT スレッド (さらには、正規 Java スレッド) が保持するロックでブロックされる場合があります。RT スレッドがロックを保持している間、GCがその RT スレッドをプリエンプトし、それによって NHRT が間接的にプリエンプトされる可能性があります。この場合、GC によって RTがプリエンプトされなくなり、RT がロックを解除して NHRT に実行の機会が与えられるまで NHRT は待たなければなりません。GC によるNHRT のプリエンプションは、NHRT がタイム・クリティカルな関数を実行している場合には深刻な問題になります。

WebSphere Real Time の確定的ガーベッジ・コレクターは一時停止時間を 1 ミリ秒未満に抑え、NHRT のプリエンプションをより確定的なものにします。このような一時停止を許容できないのであれば、NHRTとRT スレッドにロックを共有させないようにすることで問題を回避することも可能です。ロックが必須という場合は、リソースおよびロックを RT とNHRT それぞれに固有にするという手段も考えられます。例えば、スレッド・プーリングを実装するアプリケーションで個別のプールを用意し、そのそれぞれにNHRT 用のロックと RT スレッド用のロックをプールするというのも一案です。

さらに、javax.realtime パッケージには以下のクラスも用意されています。

  • WaitFreeReadQueue クラス。オブジェクトを RT スレッドから NHRT に渡すことを主な目的とします。
  • WaitFreeWriteQueue クラス。オブジェクトを NHRT から RT スレッドに渡すことを主な目的とします。

上記のクラスによって、wait のない操作の実行中に GC によってブロックされる可能性のある RT スレッドが、NHRT が wait のない操作を実行する際に必要とするロックを、保持しないように確実に行えます。

javax.realtime パッケージでの同期

特定の javax.realtime メソッドは、ロックが競合していないとしても同期のオーバーヘッドが生じるため、意図的に同期されません。同期が必要な場合は、同期されたメソッドまたはブロックに所要のjavax.realtime メソッドを、呼び出し側がラップする必要があります。プログラマーは javax.realtime パッケージのメソッドを使用する際に、そのような同期を追加することを検討しなければなりません。

コア JLS パッケージでの同期

上記とは対照的に、java.util.Vector などのコア JLS サービスはすでに同期された状態となります。また、一部のコア JLS サービスは内部ロックによって特定の共有リソースを逐次化する場合もあります。こうした同期が理由となって、コアJLS サービスを使用する際には、GC による NHRT のプリエンプションの問題 (「NHRT と RT スレッドとのロック競合」を参照) が起こらないように注意する必要があります。

非競合のロック・パフォーマンス

RT 以外のアプリケーションでのベンチマークとインスツルメンテーションでは、十中八九、ロックは競合しないことが示されています。また、特に既存のコンポーネントやライブラリーを再利用する場合などは、RTアプリケーションでは非競合のロックが圧倒的なケースだとも考えられています。競合しないことがわかっているロックのコストがわずかで確定的なことは理想的ですが、この場合、同期ディレクティブを回避または除去するのが困難です。

前述したように、非競合のロック操作にはセットアップとアトミックなマシン命令が伴い、ロック解除操作にはアトミックなマシン操作が伴います。ロック操作をセットアップするには、事前にロックされたミューテックスの割り当てが必要です。この割り当ては、非競合のロック操作では最大の変動コストだと考えられています。この変動コストを操るのに役立つのが、RealtimeSystem.setMaximumConcurrentLocks() です。

RealtimeSystem.setMaximumConcurrentLocks(int numLocks) によって、WebSphere Real Time の VM が numLocksミューテックスをグローバル・ロック・キャッシュに事前に割り当て、このグローバル・ロック・キャッシュがスレッドごとのロック・キャッシュにミューテックスをフィードするようになります。このRealTimeSystem API を使用することで、タイム・クリティカルなコード領域内でロック初期化が行われる可能性を減らすことができます。setMaximumConcurentLocks() の呼び出しで使用するロック数を判断するには RealTimeSystem.getMaximumConcurrentLocks() を使用できますが、getMaximumConcurrentLocks() が取得するのは上限基準点ではなく、呼び出しの時点でのロック使用数だということに注意してください。上限基準点を示すための API は、今後のRTSJ バージョンで提供されると考えられます。ここで注意しなければならないのは、numLocks には桁外れに大きな値を指定してはならないという点です。その理由は、setMaximimConcurrentLocks() の呼び出しが指定された数のロックを作成するために法外な時間とメモリーを消費することもあるからです。また、この API は JVM 固有になるように定義されているため、別のJVM が呼び出しを無視したり、異なる動作を行う場合があることにも注意してください。

競合のロック・パフォーマンス

1 つのスレッドが同時に複数のロックを保持する場合、これらのロックは特定の順序で取得されている可能性があります。このような一連のロック・パターンは、ロック階層を構成しています。優先度の継承がスレッド・グループの格上げと格下げを意味することがありますが、グループに含まれるスレッドの数がシステム内で考えられる最も深いロック階層を上回る数になることはありません。ロック階層を浅く保つこと、つまりできるだけロックするオブジェクトを少なくすることで、優先順位の調整が必要となるスレッドの最大数を抑えることができます。

同期操作での時間

Object.wait(long timeout, int nanos) は、相対的な待機動作にナノ秒単位の粒度を与えます。HighResolutionTime.waitForObject()Object.wait() と同様、ナノ秒単位の粒度で相対的な時間と特定の時刻を指定できます。WebSphere Real Time では、どちらの API も基本 POSIXロック待機サービスで実装されますが、これらの基本サービスが提供する粒度はせいぜいマイクロ秒単位でしかありません。粒度が要求される場合、そして移植性が必要な場合は、javax.realtime パッケージの Clock クラスの getResolution() メソッドを使用して実行プラットフォームの解像度を取得してください。

まとめ

RTSJ は javax.realtime パッケージの新しい RT クラスと API によって、Java プログラマーのためのスレッド化機能と同期機能を拡張、強化します。WebSphereReal Time では、これらの機能は RT バージョンの Linux カーネル、POSIX スレッド化の変更、そして JVM 自体の変更によって実装されます。RTSJスレッド化と同期についての理解を深めておけば、RT アプリケーションを作成、デプロイする際の問題を回避するのに役立ちます。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=250182
ArticleTitle=リアルタイム Java、第 3 回: スレッド化と同期
publish-date=04242007