レベル: 初級 Ted Neward, Principal, ThoughtWorks, Neward & Associates
2009年 4月 10日 概要: 主要なチップ・メーカーが、必ずしも高速ではないものの 2 つ以上のコアを並列に実行させるチップをリリースし始めて以来、すべてのソフトウェア開発者の頭の中にあるホットな話題として並行性が急速に登場してきました。この記事は Ted Neward が前回の「Scala での並行性を探る」のフォローアップとして、この並行性の問題を、アクターについて検証しながら深く掘り下げます。アクターとは、お互いの間でメッセージを交換し合うことで作業の調整を行う実行エンティティーです。
 |
このシリーズについて
このシリーズでは、Ted Neward が皆さんと共に Scala プログラミング言語を深く掘り下げます。この、developerWorks のシリーズでは、Scala が最近もてはやされている理由を調べ、Scala の言語機能の実際の動作を調べます。Scala のコードと Java™ のコードの比較が重要な場合には両者のコードを並べて示しますが、(これから学ぶように) Scala の機能のうちの多くは、Java には直接対応するものがありません。そして Scala の魅力の多くがあるのはそこなのです。結局のところ、Java で可能ならば、手間をかけて Scala を学ぶ必要はないのです。
|
|
前回の記事では、(Scala を使うか否かによらず) 並行処理を行うコードを作成することの重要性と、そうしたコードを作成する際に開発者が直面するいくつかの問題について説明しました。そうした問題を避けるために注意しなければならない事項には、ロックを多用しすぎない、ロックを十分に使う、デッドロックを避ける、立ち上げるスレッドの数を増やしすぎない、などさまざまなものがあります。こうしたリストを見ると気が重くなります。
私はそうした絶望感による重たい気持ちをそのままにしておく人間ではないので、並行性のために用意されている Scala の構成体の説明を始めました。まず、Java 言語の並行性ライブラリーを Scala の中から直接使うための基本的な方法を説明し、次に Scala API の MailBox タイプを説明しました。どちらの方法も確かに有効ですが、Scala と並行性がホットである本当の理由がそこにあるわけではありません。
そこに舞台の左手から Scala のアクターが登場します。
「アクター」とは何か
「アクター」実装は要するに、アクターと呼ばれる実行エンティティー同士でのメッセージ交換を利用して作業の調整を行う方法です (「プロセス」、「スレッド」、「マシン」といった言葉を意図的に避けていることに注意してください)。そう言われると何となく RPC メカニズムのように思えるかもしれませんが、アクターは RPC であるとも、RPC でないとも言えるのです。RPC での呼び出しは (Java の RMI 呼び出しと同じように)、サーバー・サイドが処理を終えて何らかの種類のレスポンス (戻り値または例外) を返送するまで呼び出し側でブロックされますが、メッセージを交換し合う方法では呼び出し側をブロックする動作を意図的に避け、そうすることで巧妙にデッドロックの発生を回避するのです。
単純にメッセージを交換し合うだけで、並行性の点で不適切なコードの問題をすべて回避できるわけではありません。またこの手法では、さまざまなアクターが共有データ構造にまったくアクセスできない「共有ゼロ」のプログラミング・スタイルが採られます (また同時に、そのアクターが JVM にローカルなものか、あるいは世界中で使用できるのかをカプセル化することにもなります)。そのため同期化がまったく必要なくなります。つまりこれまで見てきたとおり、共有するものが何もなければ、並行して実行し、同期させる必要がある対象もなくなるのです。
これは決してアクターのモデルを正式に説明したものではなく、コンピューター・サイエンスに関する正式なバックグラウンドを持つ人であれば、この説明がアクターの詳細の完全な説明としてはあらゆる点で問題があることがわかるでしょう。しかしこの記事の目的には、この説明で十分です。Web を探せば、アクターの背景にある概念を詳細に論じた学術論文をはじめ、もっと詳細で正式な説明が見つかるはずです (それらを後で調べるかどうかの判断は読者に任せることにします)。とりあえず、これで Scala のアクター API を調べる準備ができました。
Scala のアクター
基本的に、アクターの扱い方はそれほど難しくありません。アクターの扱い方を理解する最も容易な方法は、Actor クラスの actor メソッドを使って単純にアクターを作成してみることです (リスト 1)。
リスト 1. アクターを作成する
import scala.actors._, Actor._
package com.tedneward.scalaexamples.scala.V4
{
object Actor1
{
def main(args : Array[String]) =
{
val badActor =
actor
{
receive
{
case msg => System.out.println(msg)
}
}
badActor ! "Do ya feel lucky, punk?"
}
}
}
|
ここではいくつかのことが同時に行われています。
まず、自明の名前が付けられたパッケージから Scala Actors ライブラリーをインポートし、次にこのライブラリーから直接 Actor クラスのメンバーをインポートしています。この 2 番目のステップは必ずしも必要ありません。なぜなら、後ほど説明するように actor ではなく Actor.actor をコードの中で使うことができるからです。しかしこのようにすることで actor が Scala 言語に組み込みの構成体であることを印象づけられ、また (見方によっては) コードが読みやすくなります。
次のステップではアクターそのものを作成します。そのためにはコード・ブロックをパラメーターに取る actor メソッドを使用します。この場合のコード・ブロックは単純な receive を実行します (receive についてはすぐ後に説明します)。これにより、アクターが作成されて値参照の中に保存され、いつでも使用できる状態になります。
アクターが通信用にメソッドを使わずメッセージを使うことを思い出してください。直感に反するかもしれませんが、その次の ! を使用した行は実はメソッドであり、このメソッドは (実際に) メッセージを (比喩的な表現ですが) badActor に送信するのです。ベールの下で Actor trait の奥深く埋もれているのは前回説明した例の MailBox 要素の 1 つであり、! メソッドは渡されたパラメーター (この場合は String) を受け取り、それをメールボックスに入れたらすぐにリターンします。
メッセージがアクターに渡されると、アクターはそのアクターの receive メソッドを呼び出し、そのメッセージを処理します。receive メソッドは名前どおりの処理を行います。つまりメールボックスの中から先頭にあるメッセージを取り出し、それを暗黙的なパターン・マッチング・ブロックに渡します。ここではパターンに一致する場合のタイプを指定していないため、すべてのものが一致し、メッセージが msg 名にバインドされることに注目してください (msg 名は出力するために必要です)。
また、送信対象のタイプに関して制約が何もないという事実に注目してください。先ほどの例ではストリングに限定されているように見えるかもしれませんが、ストリングのみに限定されているわけではありません。実際、アクター・ベースの設計では Scala の case クラスを使って実際のメッセージ自体を渡すことがよくあり、それによってタイプに基づいて実行する暗黙的な「コマンド」または「アクション」に対して、そのアクションのパラメーターまたはデータとなる case クラスのパラメーターやメンバーが提供されます。
例えば送信されたメッセージへのレスポンスとして、2、3 の異なるアクションをアクターに実行させたいとします。この新しい実装はリスト 2 のようになります。
リスト 2. アクターに実行させる
object Actor2
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
def main(args : Array[String]) =
{
val badActor =
actor
{
receive
{
case NegotiateNewContract =>
System.out.println("I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(line)
case Gesture(bodyPart, action) =>
System.out.println("(" + action + "s " + bodyPart + ")")
case _ =>
System.out.println("Huh? I'll be in my trailer.")
}
}
badActor ! NegotiateNewContract
badActor ! Speak("Do ya feel lucky, punk?")
badActor ! Gesture("face", "grimaces")
badActor ! Speak("Well, do ya?")
}
}
|
ここまではすべてが適切で順調ですが、リスト 2 のコードを実行させると、新たな契約の交渉が行われるだけで、それが終わると JVM が終了します。初めてこれを見ると、作成されたスレッドがメッセージに応答する速度が十分ではないように思えるかもしれませんが、アクター・モデルではスレッド自体を扱わずメッセージを渡すだけであることを思い出してください。ここでの問題はむしろもっと単純なことで、1 回の receive でメッセージが 1 つだけ生成されることが問題なのです。このため、処理待ちのキューに存在するメッセージの数にかかわらず、1 回の receive があるとそれに伴い 1 つのメッセージが送信されるのみであり、複数のメッセージがキューに入れられているという事実が考慮されないのです。
この問題を修正するためには、コードを下記のように変更する必要があります (リスト 3)。
- 無限に近いループの中に
receive ブロックを配置する。
- 新しい case クラスを作成し、すべての処理がいつ終わったかが示されるようにする。
リスト 3. アクターの動作を改善する
object Actor2
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
case class ThatsAWrap;
def main(args : Array[String]) =
{
val badActor =
actor
{
var done = false
while (! done)
{
receive
{
case NegotiateNewContract =>
System.out.println("I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(line)
case Gesture(bodyPart, action) =>
System.out.println("(" + action + "s " + bodyPart + ")")
case ThatsAWrap =>
System.out.println("Great cast party, everybody! See ya!")
done = true
case _ =>
System.out.println("Huh? I'll be in my trailer.")
}
}
}
badActor ! NegotiateNewContract
badActor ! Speak("Do ya feel lucky, punk?")
badActor ! Gesture("face", "grimaces")
badActor ! Speak("Well, do ya?")
badActor ! ThatsAWrap
}
}
|
Scala のアクターをキャストに入れると映画作りさえ容易になりそうです。
アクターによる並行性
リスト 3 のコードでは明らかでないことの 1 つが、(もし並行性が存在するなら) 並行性の元はどこにあるのか、という点です。これまでに説明した内容から見る限り、このコードもまた 1 つの同期型メソッド呼び出しに見え、どこに違いがあるのかわからないかもしれません。(技術的には、無限に近いループを入れる前の 2 番目の例から、何らかの並行処理が行われていることを推測できるかもしれません。しかしそれは偶然わかるだけであり、決して確実な証明にはなりません。)
これらすべてのコードの陰にはいくつかのスレッドがあることを証明するために、先ほどの例をさらに詳しく調べてみましょう。
リスト 4. 詳しく調べる
object Actor3
{
case class Speak(line : String);
case class Gesture(bodyPart : String, action : String);
case class NegotiateNewContract;
case class ThatsAWrap;
def main(args : Array[String]) =
{
def ct =
"Thread " + Thread.currentThread().getName() + ": "
val badActor =
actor
{
var done = false
while (! done)
{
receive
{
case NegotiateNewContract =>
System.out.println(ct + "I won't do it for less than $1 million!")
case Speak(line) =>
System.out.println(ct + line)
case Gesture(bodyPart, action) =>
System.out.println(ct + "(" + action + "s " + bodyPart + ")")
case ThatsAWrap =>
System.out.println(ct + "Great cast party, everybody! See ya!")
done = true
case _ =>
System.out.println(ct + "Huh? I'll be in my trailer.")
}
}
}
System.out.println(ct + "Negotiating...")
badActor ! NegotiateNewContract
System.out.println(ct + "Speaking...")
badActor ! Speak("Do ya feel lucky, punk?")
System.out.println(ct + "Gesturing...")
badActor ! Gesture("face", "grimaces")
System.out.println(ct + "Speaking again...")
badActor ! Speak("Well, do ya?")
System.out.println(ct + "Wrapping up")
badActor ! ThatsAWrap
}
}
|
この新しい例を実行すると、次の 2 つの異なるスレッドが関係していることが明らかになります。
main スレッド (Java の main を起動するスレッドと同じもの)
- ベールの下で Scala の Actors ライブラリーによって起動された
Thread-2 スレッド
そうなのです。基本的に、あの最初のアクターを起動した場合には常にマルチスレッドで実行していたのです。
しかし、この新しい実行モデルに慣れるのは多少厄介かもしれません。何と言っても、このモデルでは並行性の考え方がまったく異なるからです。例えば前回の記事の生産者消費者モデル (Producer/Consumer モデル) を考えてみてください。このモデルには (特に Drop クラスには) かなりの量のコードがありました。そのため、スレッド同士がやり取りする際に、また同期を保つためのモニターとスレッドとがやり取りする際に、何が起きているかを非常に明確に理解することができました。参考までに前回の記事のバージョン 3 のコードを再掲します。
リスト 5. Producer と Consumer の例、バージョン 3 (Scala による方法)
package com.tedneward.scalaexamples.scala.V3
{
import concurrent.MailBox
import concurrent.ops._
object ProdConSample
{
class Drop
{
private val m = new MailBox()
private case class Empty()
private case class Full(x : String)
m send Empty() // initialization
def put(msg : String) : Unit =
{
m receive
{
case Empty() =>
m send Full(msg)
}
}
def take() : String =
{
m receive
{
case Full(msg) =>
m send Empty(); msg
}
}
}
def main(args : Array[String]) : Unit =
{
// Create Drop
val drop = new Drop()
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
);
importantInfo.foreach((msg) => drop.put(msg))
drop.put("DONE")
}
// Spawn Consumer
spawn
{
var message = drop.take()
while (message != "DONE")
{
System.out.format("MESSAGE RECEIVED: %s%n", message)
message = drop.take()
}
}
}
}
}
|
このコードの一部が Scala によって単純化された様子を見ると興味深いのですが、実はこのコードは元の Java バージョンと概念的に大幅に異なっているわけではありません。しかしここで、最低限の本質的な部分にまで簡略化した場合にアクター・ベースのバージョンでの Producer と Consumer の例がどうなるかを調べてみましょう。
リスト 6. Producer と Consumer の例 (アクターによる方法 (1))
object ProdConSample1
{
case class Message(msg : String)
def main(args : Array[String]) : Unit =
{
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("Received message! -> " + msg)
done = (msg == "DONE")
}
}
}
consumer ! "Mares eat oats"
consumer ! "Does eat oats"
consumer ! "Little lambs eat ivy"
consumer ! "Kids eat ivy too"
consumer ! "DONE"
}
}
|
この最初のバージョンは簡潔さの点では確かに申し分なく、またある状況では必要なすべてのことを行えるかもしれません。しかしこのコードを実行し、先ほどのバージョンと比較してみると、大きな違いがあることがわかります。つまりアクター・ベースのバージョンのバッファーは複数の場所に存在しており、以前のバージョンのように Drop 1 ヶ所ではありません。これは機能強化であって欠点ではないと思う人がいるかもしれませんが、同じ土俵で比較してみましょう。少し前に戻り、アクター・ベースのバージョンの Drop を作成してみましょう。この場合は put() を呼び出すごとに take() を呼び出す必要があります。
幸いなことに、Scala の Actors ライブラリーを使うと、この機能を非常に容易に再現することができます。基本的に、Consumer がメッセージを受信するまで Producer をブロックする必要があります。この処理を最も容易に行うためには、メッセージを受信したという Consumer からの確認応答を受信するまで Producer をブロックします。これはある意味で、先ほどのモニター・ベースのコードが行っていることです (モニター・ベースのコードでは、そうしたシグナリング動作をロック・オブジェクトの前後でモニターを使って行っています)。
これを Scala の Actors ライブラリーで行うための最も容易な方法は、! メソッドを使う代わりに、(確認応答が受信されるまでブロックする) !? メソッドを使う方法です。(関心のある人のために説明すると、Scala の Actors 実装では、すべての Java スレッドは既にアクターです。そのため、暗黙的に main スレッドと関連付けられているメールボックスに応答が受信されます。) これはつまり、Consumer が何らかの確認応答を送信する必要があるということであり、Consumer はそれを、Consumer が (receive メソッドと共に) 暗黙的に継承する reply メソッドを使って行います (リスト 7)。
リスト7. Producer と Consumer の例 (アクターによる方法 (2))
object ProdConSample2
{
case class Message(msg : String)
def main(args : Array[String]) : Unit =
{
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("Received message! -> " + msg)
done = (msg == "DONE")
reply("RECEIVED")
}
}
}
System.out.println("Sending....")
consumer !? "Mares eat oats"
System.out.println("Sending....")
consumer !? "Does eat oats"
System.out.println("Sending....")
consumer !? "Little lambs eat ivy"
System.out.println("Sending....")
consumer !? "Kids eat ivy too"
System.out.println("Sending....")
consumer !? "DONE"
}
}
|
あるいは、spawn を使って Producer を起動し、main() とは別のスレッドに入れる方法 (この方法は元の方法を最もよく反映しています) を好む場合には、リスト 8 のようになります。
リスト 8. spawn を使った Producer と Consumer の例 (アクターによる方法)
object ProdConSampleUsingSpawn
{
import concurrent.ops._
def main(args : Array[String]) : Unit =
{
// Spawn Consumer
val consumer =
actor
{
var done = false
while (! done)
{
receive
{
case msg =>
System.out.println("MESSAGE RECEIVED: " + msg)
done = (msg == "DONE")
reply("RECEIVED")
}
}
}
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too",
"DONE"
);
importantInfo.foreach((msg) => consumer !? msg)
}
}
}
|
どのように見てもアクター・ベースのバージョンの方が元の方法よりもはるかに単純ですが、アクター・ベースのバージョンが単純さを保つには、アクターと暗黙的なメールボックスを適切に維持する必要があります。
これは些細な点ではありません。アクター・モデルでは並行性とスレッド・セーフの考え方のプロセス全体が完全に逆なのです。アクター・モデルによって、共有データ構造 (データの並行性) に焦点を当てるモデルから、データ (タスクの並行性) に対して動作し、データを極力共有しないコードの構成自体に焦点を当てるモデルに変わるのです。この反転が先ほどのコードの Producer/Consumer の例の中にあることに注目してください。これまでの例では Drop クラス (境界が定められたバッファー) の前後に並行性が明示的に書かれていました。この記事の、このバージョンでは、Drop は登場することもなく、焦点は、2 つのアクター (スレッド) と、共有ゼロのメッセージを介したアクター同士のやり取りに絞られたままです。
当然ですが、アクターを使ってデータ中心の並行性構成体を作成することは相変わらず可能であり、そのためには少し異なる手法を取ればよいだけです。例えば次の単純なカウンター・オブジェクトを考えてみてください。このオブジェクトはアクター・メッセージを使ってインクリメント操作と取得操作の伝達を行います (リスト 9)。
リスト 9. Counter の例 (アクターによる方法)
object CountingSample
{
case class Incr
case class Value(sender : Actor)
case class Lock(sender : Actor)
case class UnLock(value : Int)
class Counter extends Actor
{
override def act(): Unit = loop(0)
def loop(value: int): Unit = {
receive {
case Incr() => loop(value + 1)
case Value(a) => a ! value; loop(value)
case Lock(a) => a ! value
receive { case UnLock(v) => loop(v) }
case _ => loop(value)
}
}
}
def main(args : Array[String]) : Unit =
{
val counter = new Counter
counter.start()
counter ! Incr()
counter ! Incr()
counter ! Incr()
counter ! Value(self)
receive { case cvalue => Console.println(cvalue) }
counter ! Incr()
counter ! Incr()
counter ! Value(self)
receive { case cvalue => Console.println(cvalue) }
}
}
|
あるいはもっと Producer と Consumer の例に近いものにすると、リスト 10 は (例えばアクター・メソッドを直接呼び出す方法を気にせずに他の Java クラスで Drop を使えるようにするために) 内部でアクターを使用するバージョンの Drop です。
リスト 10. 内部でアクターを使用する Drop
object ActorDropSample
{
class Drop
{
private case class Put(x: String)
private case object Take
private case object Stop
private val buffer =
actor
{
var data = ""
loop
{
react
{
case Put(x) if data == "" =>
data = x; reply()
case Take if data != "" =>
val r = data; data = ""; reply(r)
case Stop =>
reply(); exit("stopped")
}
}
}
def put(x: String) { buffer !? Put(x) }
def take() : String = (buffer !? Take).asInstanceOf[String]
def stop() { buffer !? Stop }
}
def main(args : Array[String]) : Unit =
{
import concurrent.ops._
// Create Drop
val drop = new Drop()
// Spawn Producer
spawn
{
val importantInfo : Array[String] = Array(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
);
importantInfo.foreach((msg) => { drop.put(msg) })
drop.put("DONE")
}
// Spawn Consumer
spawn
{
var message = drop.take()
while (message != "DONE")
{
System.out.format("MESSAGE RECEIVED: %s%n", message)
message = drop.take()
}
drop.stop()
}
}
}
|
リスト 10 を見るとわかるように、この方法はより多くのコードを必要とし、(さらに各アクターがスレッド・プール内で動作するため追加のスレッドも必要ですが)、このバージョンはこれまでに作成したバージョンと API が同じであり、並行性に関するすべての懸念事項は Java 開発者が通常想定する場所である Drop クラスの中に入っています。
アクターについては他にも説明すべきことがあります。
大規模なシステムの内部などの場合には、各アクターを裏で支える Java スレッドを用意すると (特に各アクターが処理を行っている時間よりも処理を行うために待機している時間の方が長くなりそうな場合には)、重くなりすぎ、また無駄が多すぎることになります。こうした状況ではイベント・ベースのアクターが適切かもしれません。このアクターは実質的にクロージャーの内部に置かれ、クロージャーがそのアクターの他のアクションを取り込みます。つまりコード・ブロック (関数) をスレッドの状態やレジスターなどを使って表す必要はないということです。クロージャーはアクターがメッセージを受信すると起動されます (このためには当然ながらアクティブなスレッドが必要です)。クロージャーは動作を行う間、アクティブなスレッドを借用し、動作が終わったら終了するか、あるいはクロージャー自身をコールバックすることで「待機」状態に入るかし、実質的にそのスレッドをこのクロージャー以外のものが使えるように解放するのです。(「参考文献」に挙げた Haller/Odersky の論文を参照してください。)
この動作は Scala の Actors ライブラリーの中で、この記事で説明した receive. の代わりに react メソッドを使って行われます。react を使う上での鍵は、react は正式には return することができないため、react ブロックを含むコード・ブロックを react 内の実装から再度呼び出すようにしなければなりません。この場合、ほとんど無限のループを作成してくれる (名前どおりの) loop 構成体が便利です。これはつまり、リスト 10 の Drop 実装が、実際には呼び出し側のスレッドを借用するだけで動作することができ、必要なすべての操作を実行する上で必要となるスレッドの数を減らせるということです。(現実には、簡単な例で実際にそうなる様子を私は見たことがないので、Scala 設計者の意見を聞いてみる必要がありそうです。)
場合によると、Actor のベース trait から継承し (その場合には act メソッドを定義する必要があり、そうしないとクラスが抽象クラスのままになります)、暗黙的にアクターとして動作する新しいクラスを作成する選択肢もあります。とは言っても、その考え方は Scala のコミュニティーの支持を得ていません。一般的には、新しいアクターを作成する方法としては私が概要を説明した (Actor オブジェクトの actor メソッドを使う) 方法が支持されています。
まとめ
アクターを使ったプログラミングには「従来の」オブジェクトを使ったプログラミングとは少し異なるスタイルが必要なため、アクターを扱う場合にはいくつかのことを頭に入れておく必要があります。
第 1 に、アクターの強力さの大部分はメッセージ・パッシング・スタイルに依存していることを忘れてはなりません。このスタイルは他の命令型プログラミングの世界の特徴である、ブロックして呼び出すというスタイルとは異なります。(興味深いことに、中核的な原理としてメッセージ・パッシングを使うオブジェクト指向言語が存在します。そうしたものの中で最も広く知られている 2 つが Objective-C と Smalltalk であり、また新たに、ThoughtWorker の Ola Bini が作成した Ioke も登場してきています。) Actor を直接的に、あるいは間接的に継承するクラスを作成する場合には、対象のオブジェクトに対するすべての呼び出しが必ずメッセージ・パッシングによって行われるようにします。
第 2 に、メッセージは任意のタイミングで送信される可能性があり、(そしてもっと重要な点として) 送信と受信の間に大きな遅延が発生する場合があるため、メッセージが適切に処理されるようにするには、必要な状態をすべて確実にそのメッセージに保持させる必要があります。そのためには以下を行う必要があります。
- コードを理解しやすいものにする (なぜなら、処理に必要なすべての状態をそのメッセージが保持するため)。
- アクターが他の場所で共有状態にアクセスする可能性を減らし、デッドロックなど並行性に関する悪夢を発生しにくくする。
第 3 に、これまでの説明から少なからず明確なはずですが、アクターがブロック動作をしてはならないことを指摘しておく必要があります。問題の核心として、デッドロックはブロック動作によって起こります。コードのブロック動作を避ければ避けるほど、デッドロックを回避できる可能性が高くなります。
非常に興味深いことに、JMS (Java Message Service) API をよく理解している人であれば、上記の推奨事項が JMS の場合とよく似ていることに気付くでしょう。結局のところ、アクターのメッセージ・パッシング・スタイルは、単にエンティティーの間でメッセージが交換されるということにすぎません (これは JMS でのメッセージ・パッシングが単にエンティティーの間でのメッセージ交換にすぎないのと同じことです)。JMS との違いは、JMS メッセージは規模が大きいことが多く、階層やプロセスのレベルで操作されますが、アクターのメッセージは規模が小さいことが多く、オブジェクトやスレッドのレベルで操作されます。JMS を理解できるとアクターも理解することができます。
アクターは皆さんのコードに起こりうる並行性の問題をすべて解決する万能薬ではありませんが、アプリケーションやライブラリー・コードをモデリングするための新たな方法であることは確かであり、使用される構成体は見かけも動作もかなり単純でわかりやすくなっています。だからといってアクターが必ず思いどおりに動作するというわけではありませんが、その動作の一部を想定することは可能です。言ってみれば、皆さんがオブジェクトを初めて扱った際にもオブジェクトは思いどおり動作しなかったのと同じなのです。
今回はこれで終わりです。では次回をお楽しみに。
参考文献 学ぶために
製品や技術を入手するために
- Scala をダウンロードし、このシリーズと共に Scala を学んでください。
- SUnit は Scala の標準的なディストリビューションの一部であり、scala.testing パッケージの中にあります。
議論するために
著者について  | 
|  | Ted Neward は世界規模でコンサルティングを行う ThoughtWorks のコンサルタントであり、また Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住です。 |
記事の評価
|