Java 開発 2.0: Kilim の紹介

Java の並行性のためのアクター・フレームワーク

並行プログラミングは Java™ 開発 2.0 の中心となっていますが、おそらくこれは、スレッド・ベースの並行性ではありません。今回の記事では、Andrew Glover がマルチコア・システムにおける並行プログラミングでアクターがスレッドに勝る理由を説明し、並行プログラミングと分散プログラミングを 1 つに組み合わせたアクター・ベースのメッセージ・パッシング・フレームワーク Kilim を紹介します。

Andrew Glover, Author and developer

Andrew GloverAndrew Glover は、ビヘイビア駆動開発、継続的インテグレーション、アジャイル・ソフトウェア開発に情熱を持つ開発者であるとともに、著者、講演者、起業家でもあります。また、easyb BDD (Behavior-Driven Development) フレームワークの創始者、そして「継続的インテグレーション入門 開発プロセスを自動化する47の作法」、「Groovy in Action」、「Java Testing Patterns」の 3 冊の本の共著者でもあります。詳細は彼のブログにアクセスしてください。



2010年 4月 13日

マルチスレッド・アプリケーションで非決定的な欠陥をデバッグするという作業は、ソフトウェア開発者にとって最も厄介で苛立たしい作業の筆頭に挙げられます。そんな事情から、私も大勢のソフトウェア開発者と同じく、Erlang や Scala などの関数型言語での並行プログラミングにくぎ付けになっています。

Scala と Erlang が並行プログラミングに採用しているのはどちらもスレッドではなく、アクター・モデルです。アクター・モデルに関する革新は言語だけにとどまりません。アクター・モデルは、Kilim のような Java ベースのアクター・フレームワークでも使用することができます。

アクター・モデルに対する Kilim の手法は直観的であり、この後すぐにわかるように、このライブラリーによって並行アプリケーションをごく簡単に構築できるようになります。

マルチコアでの難題

2005年に Herb Sutter が書いた「The Free Lunch is Over: A Fundamental Turn Toward Concurrency in Software」は、今では有名な記事です。彼はこの記事のなかで、ムーアの法則によってこれからも限りなく CPU のクロック速度が上がっていくはずだという根拠のない確信に反論しています。

Sutter は、どんどん高速化されていくチップに頼ることで、何の苦労もなくソフトウェア・アプリケーションのパフォーマンスが向上するという「ただ飯」の時代は終わると予測しました。そして、今後、アプリケーションのパフォーマンスを目に見えて向上させるには、マルチコア・チップ・アーキテクチャーを利用する必要があるだろうと彼は言っています。

現在明らかになっているように、彼の予測は当たっていました。チップ・メーカーはチップ速度の限界に達し、その速度はここ数年、3.5 GHz あたりにとどまっています。しかし、ムーアの法則はマルチコアの領域では生きていて、チップ・メーカーがチップに搭載するコア数は増加の一途を辿っています。

この連載について

Java 技術が初めて登場してから現在に至るまでに、Java 開発の様相は劇的に変化しました。成熟したオープンソースのフレームワーク、そしてサービスとして提供される信頼性の高いデプロイメント・インフラストラクチャーを利用できる (借りられる) おかげで、今では Java アプリケーションを短時間かつ低コストでアセンブルし、テスト、実行、保守することが可能になっています。この連載では Andrew Glover が、この新たな Java 開発パラダイムを可能にする多種多様な技術とツールを詳しく探ります。

また、Sutter は並行プログラミングによって、開発者はマルチコア・アーキテクチャーのメリットを生かすことができるとも言っていますが、「そのためには、現在の言語が提供しているプログラミング・モデルよりも、上位レベルで並行性に対応するプログラミング・モデルが必須である」と言い添えています。

Java のような言語での基本プログラミング・モデルはスレッド・ベースです。マルチスレッド・アプリケーションを作成するのは至難の業というわけではありませんが、「適切に」作成する上ではいくつかの難題があります。並行プログラミングで何が難しいかと言えば、スレッドによる並行性の観点から考えることです。こうした点で、スレッドに代わる並行性モデルとしていくつかのモデルが登場しました。なかでも特に興味深く、しかも Java コミュニティーで賛同されているモデルがアクター・モデルです。


アクター・モデル

アクター・モデルは、並行プロセスをモデル化する 1 つの方法です。アクター・モデルでは、ロックを使用した共有メモリーを介して相互作用するスレッドではなく、メール・ボックスを使用して非同期メッセージの受け渡しを行う「アクター」を利用します。この場合のメール・ボックスは実生活でのメール・ボックスとまったく同じです。メール・ボックスにはメッセージを保管することも、別のアクターがメール・ボックスからメッセージを取得して処理することもできます。メモリー内で変数を共有する方法とは異なり、メール・ボックスは個々のプロセスを効果的に分離します。

アクターは独立した個々のエンティティーとして機能し、アクターの間のやり取りは、メモリーを共有することなく行われます。実際、アクター相互のやり取りに使用される手段はメール・ボックスだけです。アクター・モデルにはロックも、synchronized ブロックもないため、この 2 つによって生じる問題 (デッドロックや更新が不正に消失するなどの問題) は、アクター・モデルには存在しません。その上、アクターは何らかの順序で動作するのではなく、同時に動作するように意図されています。このように、アクターは遥かに安全で (ロックや同期化が必要ないため)、アクター・モデルそのものが、調整という問題に対処します。要するに、アクター・モデルは並行プログラミングを容易なものにするのです。

アクター・モデルは決して新しいモデルではなく、かなり以前からありました。Erlang や Scala などの一部の言語では、並行性モデルのベースをスレッドではなく、アクターに置いています。実際には、数々の企業における Erlang の成功が (Erlang は Ericsson によって作成され、電気通信の世界では十分な実績があります)、アクター・モデルの人気と認知度を高め、その結果、他の言語でもアクター・モデルが選択可能になったとも言えます。Erlang は、アクター・モデルによって一層安全な並行性を実現している代表例です。

アクター・モデルは、残念ながら Java プラットフォームには組み込まれていませんが、さまざまな形で使用できるようになっています。JVM は代替言語に関してオープンであり、このことは Scala や Groovy などの Java プラットフォーム言語でアクターを利用できることを意味します (Groovy のアクター・ライブラリーである GPars については「参考文献」を参照)。さらに、アクター・モデルを可能にする Java ベースのライブラリーを試してみることもできます。そのうちの 1 つが、Kilim です。


Kilim でのアクター

Java で作成された Kilim は、アクター・モデルを具現化したライブラリーです。Kilim での「アクター」は、Kilim の Task 型によって表されます。Task は軽量のスレッドであり、Kilim の Mailbox 型を介して他の Task とやり取りします。

Mailbox は、あらゆる型の「メッセージ」を受け入れることができます。例えば、Mailboxjava.lang.Object を受け入れます。TaskString 型のメッセージであろうと、さらにはカスタム型のメッセージであろうと送信することができます。どのようなメッセージを送信するかは、完全に開発者次第です。

Kilim 内では、すべての要素がメソッド・シグニチャーによってひとつにまとめられます。何かを同時に処理する必要がある場合には、メソッド・シグニチャーを増補して Pausable をスローすることによって、この振る舞いを指定します。このように、Kilim で並行クラスを作成するのは Java で Runnable を実装したり、Thread を継承したりするのと同じく簡単です。ただ、RunnableThread を使用するために必要なもの (キーワード synchronized など) がすべて不要になるだけのことです。

最後に付け加えておく点として、Kilim の巧みな仕組みを可能にするのは、クラスのバイト・コードを変更する weaver という名前の事後プロセスです。Pausable throw 節が含まれるメソッドは、実行時に Kilim ライブラリーの一部となっているスケジューラーによって処理されます。カーネル・スレッドの制限数を操作するこのスケジューラーは、このスレッド・プールを利用してより多くの軽量スレッドに対応できるため、極めて短時間でコンテキスト・スイッチを行ってスレッドを起動することができます。各スレッドのスタックは、自動的に管理されます。

基本的に、Kilim によって並行プロセスの作成は容易で単純な作業になり、Kilim の Task 型を継承して、execute メソッドを実装するだけの作業となります。新しく作成した並行処理対応クラスをコンパイルしたら、そのクラスに対して Kilim の weaver を実行すれば、後は順調に事が運びます。

Kilim は、初めは少しとっつきにくいライブラリーですが、最終的には大きな見返りがもたらされます。アクター・モデル (つまり Kilim) によって、同じようなオブジェクトに依存して非同期に動作するオブジェクトをより簡単かつ安全に作成できるようになるからです。Java の基本スレッド・モデルでも同じことをできますが (Thread を継承するなど)、その場合にはロックと同期化の世界に再び戻されてしまうため、厄介な作業になってしまいます。要するに、並行プログラミング・モデルをアクターに切り替えることで、マルチスレッド・アプリケーションのコーディングが容易になるということです。


Kilim の動作

Kilim のアクター・モデルでは、メッセージは Mailbox を介してプロセス間で受け渡しされます。多くの点で、Mailbox はキューとみなすことができます。プロセスはメッセージをメール・ボックスに入れられるだけでなく、メール・ボックスからメッセージを取り出すこともできます。しかもこれは、ブロック方式と非ブロック方式の両方で行われます (ブロックされるのは、Kilim によって実装されたベースとなる軽量プロセスで、カーネル・スレッドではありません)。

Kilim でメール・ボックスを使用する一例として、私は Kilim の Task 型を継承する 2 つのアクター (CalculatorDeferredDivision) を作成しました。この 2 つのクラスは、並行する形で連動します。DeferredDivision オブジェクトは被除数と除数を作成しますが、この 2 つの除算を試みることとはしません。除算にはコストがかかることから、DeferredDivision オブジェクトはこのタスクを Calculator 型に処理してもらうようにします。

2 つのアクターは、共有 Mailbox インスタンスを介してやり取りします。このインスタンスが受け入れるのは、Calculation 型です。このメッセージ型は至って単純なもので、被除数と除数が提供されると、Calculator が計算を行って、対応する計算結果を Calculation インスタンスに設定します。その後、Calculator はこの Calculation インスタンスを共有 Mailbox に戻します。

Calculation

リスト 1 に、単純な Calculation 型を記載します。このリストを見ると、この型には特殊な Kilim コードが必要ないことに気付くはずです。実のところ、これはありふれた Java Bean にすぎません。

リスト 1. Calculation 型のメッセージ
import java.math.BigDecimal;

public class Calculation {
 private BigDecimal dividend;
 private BigDecimal divisor;
 private BigDecimal answer;

 public Calculation(BigDecimal dividend, BigDecimal divisor) {
  super();
  this.dividend = dividend;
  this.divisor = divisor;
 }

 public BigDecimal getDividend() {
  return dividend;
 }

 public BigDecimal getDivisor() {
  return divisor;
 }

 public void setAnswer(BigDecimal ans){
  this.answer = ans;
 }

 public BigDecimal getAnswer(){
  return answer;
 }

 public String printAnswer() {
  return "The answer of " + dividend + " divided by " + divisor +
    " is " + answer;	
 }
}

DeferredDivision

Kilim 固有のクラスは、DeferredDivision クラスで登場します。このクラスはさまざまなことを行いますが、概要としては、このクラスが行うジョブの内容は単純で、乱数 (BigDecimal 型) を使用して Calculation のインスタンスを作成し、そのインスタンスを Calculator アクターに送信するというものです。さらに、このクラスは共有 MailBox. の中に Calculation があるかどうかを調べます。見つかった Calculation インスタンスに計算結果が設定されている場合、DeferredDivision はその計算結果を出力します。

リスト 2. ランダムな除数と被除数を作成する DeferredDivision
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Date;
import java.util.Random;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class DeferredDivision extends Task {

 private Mailbox<Calculation> mailbox;

 public DeferredDivision(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  Random numberGenerator = new Random(new Date().getTime());
  MathContext context = new MathContext(8);
  while (true) {
   System.out.println("I need to know the answer of something");
   mailbox.putnb(new Calculation(
     new BigDecimal(numberGenerator.nextDouble(), context), 
     new BigDecimal(numberGenerator.nextDouble(), context)));
   Task.sleep(1000);
   Calculation answer = mailbox.getnb(); // no block
   if (answer != null && answer.getAnswer() != null) {
    System.out.println("Answer is: " + answer.printAnswer());
   }
  }
 }
}

リスト 2 を見るとわかるように、DeferredDivision クラスは Kilim の Task 型を継承するため、基本的にアクター・モデルをエミュレートします。このクラスは、デフォルトで Pausable をスローする Task の execute メソッドをオーバーライドすることにも注意してください。したがって、execute のアクションは Kilim のスケジューラーによって制御されることになります。つまり Kilim によって、execute が安全に並行して機能することが確実になるのです。

execute メソッドの内部では、DeferredDivisionCalculation のインスタンスを作成し、作成したインスタンスを Mailbox に入れます。この操作は、putnb メソッドを使用して非ブロック方式で行われます。

mailbox にインスタンスを入力すると、DeferredDivision はスリープ状態になります。カーネル・スレッドがスリープ状態になるのではなく、Kilim が管理する軽量のスレッドがスリープ状態になるという点に注意してください。このアクターがウェイクアップすると、前に説明したように、mailboxCalculation の有無をチェックします。この呼び出しも同じく非ブロック方式で行われるので、getnbnull を返すことができます。DeferredDivisionCalculation インスタンスを検出し、そのインスタンスの getAnswer メソッドに値が設定されている場合には (つまり、Calculator 型による処理がまだ必要な Calculation インスタンスではない場合)、その値がコンソールに出力されます。

Calculator

Mailbox の反対側にあるのは Calculator です。リスト 2 に定義されている DeferredDivision アクターと同じく、Calculator アクターは Kilim の Task を継承して execute メソッドを実装します。重要な点は、この 2 つのアクターは同じ Mailbox インスタンスを共有することです。それぞれに別の Mailbox を使ってやり取りすることはできないため、インスタンスを共有しなければなりません。したがって両方のアクターが、それぞれのコンストラクターによって型定義された Mailbox を受け入れます。

リスト 3. いよいよ実際のワーカーである Calculator の出番です
import java.math.RoundingMode;

import kilim.Mailbox;
import kilim.Pausable;
import kilim.Task;

public class Calculator extends Task{

 private Mailbox<Calculation> mailbox;

 public Calculator(Mailbox<Calculation> mailbox) {
  super();
  this.mailbox = mailbox;
 }

 @Override
 public void execute() throws Pausable, Exception {
  while (true) {			
   Calculation calc = mailbox.get(); // blocks
   if (calc.getAnswer() == null) {
    calc.setAnswer(calc.getDividend().divide(calc.getDivisor(), 8, 
      RoundingMode.HALF_UP));				
    System.out.println("Calculator determined answer");
    mailbox.putnb(calc);
   }
   Task.sleep(1000);
  }
 }
}

Calculatorexecute メソッドは、DeferredDivision の場合と同じように、継続的にループ処理を行って共有 Mailbox でメッセージを検索します。DeferredDivision と異なる点は、Calculatorget メソッドを呼び出すことです。これはブロック方式の呼び出しであり、Calculation の「メッセージ」を検出すると、Calculator は必要とされる除算を行います。そして最後に、Calculator は変更後の Calculation を (非ブロック方式で) Mailboxに戻した後、中断します。両方のアクターでスリープ呼び出しが行われているのは、単にコンソールを容易に読み取れるようにするためです。


Kilim の weaver

前に、Kilim は weaver でバイト・コードを操作することによって機能すると説明しました。weaver は単なる事後プロセスで、コンパイルした後のクラスに対して実行します。weaver を実行すると、Pausable アノテーションが付けられた各種のクラスとメソッドに、特殊なコードが追加されます。

weaver を呼び出すのは簡単です。例えばリスト 4 では、Ant を使用して Weaver を呼び出しています。必要な作業は、Weaver に対して、対象とするクラスが置かれている場所と、処理した後のバイト・コードを格納する場所を指示することだけです。このリストでは、Weaver に target/classes ディレクトリーにあるクラスを変更して、結果のバイト・コードを同じディレクトリーに書き込むように指示しています。

リスト 4. Kilim の weaver を Ant で呼び出す
<target name="weave" depends="compile" description="handles Kilim byte code weaving">
 <java classname="kilim.tools.Weaver" fork="yes">
  <classpath refid="classpath" />
  <arg value="-d" />
  <arg value="./target/classes" />
  <arg line="./target/classes" />
 </java>
</target>

コードが変更されたら、クラスパスに Kilim の.jar ファイルを含めておけば、あとは実行時に自由に Kilim を利用できるようになります。


実行時の Kilim

2 つのアクターを実行に移す方法は、典型的な 2 つの Thread を Java コードで起動する場合と同じです。同じ共有 sharedMailbox インスタンスを使用して、アクターの 2 つのインスタンスの作成と初期値の設定を行った後、start メソッドを呼び出してアクターを実行状態にします。

リスト 5. 単純な実行プログラム
import kilim.Mailbox;
import kilim.Task;

public class CalculationCooperation {
 public static void main(String[] args) {
  Mailbox<Calculation> sharedMailbox = new Mailbox<Calculation>();

  Task deferred = new DeferredDivision(sharedMailbox);
  Task calculator = new Calculator(sharedMailbox);

  deffered.start();
  calculator.start();

 }
}

この 2 つのアクターを実行すると、リスト 6 のような内容が出力されることになります。皆さんがこのコードを実行した場合の出力は多少異なるかもしれませんが、アクティビティーのロジック・シーケンスが表示されることには変わりありません。リスト 6 では、DeferredDivision が計算を要求し、Calculator が計算結果で応答しています。

リスト 6. アクター実行時の出力 (アクターの非決定性により、出力内容は変わります)
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.36477377 divided by 0.96829189 is 0.37671881
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.40326269 divided by 0.38055487 is 1.05967029
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.16258913 divided by 0.91854403 is 0.17700744
[java] I need to know the answer of something
[java] Calculator determined answer
[java] Answer is: The answer of 0.77380722 divided by 0.49075363 is 1.57677330

まとめ

アクター・モデルは、プロセス (または、アクター) 間でのメッセージ・パッシング・メカニズムをより安全にすることによって、並行プログラミングを容易に行えるようにします。このモデルの実装は、言語やフレームワークによって異なるので、まずは Erlang のアクターを調べてから、次に Scala のアクターを調べてみてください。どちらのアクターも、それぞれの言語の構文を基にした極めて簡潔な実装となっています。

「ごく普通の」Java アクターを使用したいという場合には、Kilim やこれと同様のフレームワーク (「参考文献」を参照) がまさにぴったりです。「ただ飯」というわけにはいきませんが、アクター・ベースのフレームワークは、並行プログラミング、そしてマルチコア・プロセスをはるかに簡単に使用できるようにしてくれます。

参考文献

学ぶために

  • A Fundamental Turn Toward Concurrency in Software」(Herb Sutter 著、Dr. Dobb's Journal、2005年3月): Herb Sutter が並行プログラミングの新しい手法を論証した最初の記事です。
  • More Java Actor Frameworks Compared」(Salmon Run、2009年1月): Sujit Pal がそのブログ投稿で、Kilim、Jetlang、ActorFoundry、Actors Guild を例に、Java ベースのアクター・フレームワークと Scala ベースのアクター・フレームワークを比較しています。
  • Understanding actor concurrency, Part 1: Actors in Erlang」(Alex Miller 著、JavaWorld、2009年2月): Erlang および Scalaで実装された場合のアクター・モデルについて詳しく検討しています。
  • Crossing borders: Concurrent programming with Erlang」(Bruce Tate 著、developerWorks、2006年4月): 並行プログラミング、分散システム、およびソフト・リアルタイム・システムで Erlang がよく使用されていることになっている根拠を調べてください。
  • 多忙な Java 開発者のための Scala ガイド: Scala での並行性を探る」(Ted Neward 著、developerWorks、2009年2月): Ted Neward が、Scala 言語および環境が提供するさまざまな並行性機能とライブラリーについて説明しています。
  • Kilim の詳細: Kilim を作成した Sriram Srinivasan がホストするこのページには、Kilim の概要に加え、ホワイトペーパー、チュートリアル、ビデオによるプレゼンテーションへのリンクも記載されています。
  • Technology bookstore で、この記事で取り上げた技術やその他の技術に関する本を探してください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • GPars (Groovy Parallel Systems): Groovy のアクター・ベースのフレームワークを使って、Java プラットフォームで並行プログラミングを始めてください。
  • Kilim のダウンロード: 極めて軽量のスレッドと、そしてこれらのスレッド間で素早く安全にコピーを使わずにメッセージングを行う機能を提供する、Java 対応のメッセージ受け渡しフレームワークです。

議論するために

コメント

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=490071
ArticleTitle=Java 開発 2.0: Kilim の紹介
publish-date=04132010