LL脳がscalaの勉強を始めたよ その103


Scalaコップ本の30章に入っていきますよー。30章はアクターと並行プログラミングってことで、並列処理に関するエトセトラをやっていきますよー

並列処理とか余りやったことがないのでじっくりとやってきますかねー

楽園のトラブル

Javaの世界でのマルチスレッドプログラミングへの対応状況とその問題点をサラサラッと

Javaプラットフォームでのマルチスレッド

Javaプラットフォームでのマルチスレッドの処理では、個々のオブジェクトに対応付けられた論理モニターがデータに対するマルチスレッドアクセスをコントロールするとのことです。これを共有データ・ロックモデルと呼ぶそうです。

共有データ・ロックモデルの具体的な動きとしては、共有データにアクセスできるロックによって保護されたsynchronizedセクションに同時に入れるスレッドを、synchronized修飾子を付与されたコードセクションの中から1つだけに制限して、共有データに対するマルチスレッドアクセスをコントロール出来るようにしているそうです。

共有データ・ロックモデルの難点

共有データ・ロックモデルではアプリケーションの規模や複雑度が増してくると、信頼性の高い頑丈なマルチスレッド処理を実現するのが難しくなるみたいです。

この複雑なマルチスレッド処理実現の難しさは、次のような要素が原因となるみたいです

  • 他スレッドからの共有データ変更への対処
    • プログラムのあちこちで自分が利用しているデータのうち、他のスレッドから変更される可能性のある箇所はどこか?どのようなロックがかけられているのか?を考慮する必要がある
  • デッドロックの回避
    • メソッド呼び出しの度に、デッドロックに陥る事のないように獲得しようとしているロックが何かを確認して対処する必要がある
    • プログラム中のロックは実行中に自由に生成できるので、コンパイル時にチェックすることが出来ない
  • テストの信頼性が低い
    • マルチスレッドのコードは、スレッドが非決定論的であるため1000回のテストに成功しても1001回目の本番実行で失敗する可能性があるというふうにテストの信頼性が低いという問題がある
  • 同期をとり過ぎた場合の問題
    • 全てのアクセスの同期をとるようにして、同期をとるたびにロックを追加すれば競合が起きる可能性が低下するもののデッドロックが発生する可能性が高くなる
    • ようするにバランスの問題
Javaでの対処

Java5では従来よりも高水準の抽象モデルを提供する並行処理ユーティリティーライブラリとしてjava.util.concurrentを導入してエラーを起こしにくくしているみたいです...が、こちらも共有データ・ロックモデルを基礎としているので根本的な問題点の解決にはいてってないのだとか(´・ω・`)

Scalaはアクターで対処するのです

Scalaでは共有データ・ロックモデルではなく、アクターという概念を取り入れることで上記のような問題を解決しているみたいです。Scalaのアクターライブラリでは共有なしメッセージ交換モデルを採用することで、デッドロックや競合を回避していくとのことです。

アクターとメッセージ交換

それでは実際にアクターについて見ていきましょうかねー

「アクターは、スレッドに似たエンティティで、メッセージを受け取るためのメールボックスを持っている」というのがコップ本での説明なのですが、これだけ見ていると個々のアクターがソレゾレメッセージをキャッチボールのようにやり取りして処理を進めていくようなイメージになりますね(´・ω・`)

まあ、文章だけでだとイメージがつかみにくいので、とりあえず簡単なアクターをサンプルで書いてみますです。アクターを実装するためにはscala.actors.Actorサブクラスをつくって、actメソッドを実装すればいいみたいですね

import scala.actors._
// scala.actors.Actorサブクラスを作ります
object SillyActor extends Actor {
  // actメソッドを実装します
  def act(){
    // このアクターはメッセージを5回出力しますね
    for(i <- 1 to 5){
      println("俺がアクターです")
      Thread.sleep(1000)
    }
  }
}

上記はメールボックスを使用しないでメッセージをぶん投げるだけの単純なアクターになりますデス。とりあえず実行してみますね(´・ω・`)アクターの実行はstartメソッドを実行すればOKみたいです

// 実行します
scala> SillyActor.start
res0: scala.actors.Actor = SillyActor$@88b858

// 1秒間隔でメッセージが投げられますね(`・ω・´)
// これは別スレッドで実行されております
scala> 俺がアクターです
俺がアクターです
俺がアクターです
俺がアクターです
俺がアクターです

上記実行結果ではメッセージの出力の前にScalaシェルの出力が割り込んでいいますデス。これはSillyActorアクターがシェルを実行しているスレッドとは別のスレッドとして実行されているからみたいです。

また、複数のアクター同士も別のスレッドで実行されるみたいです。例えば次のような新しいアクターを定義して試してみますよ(`・ω・´)

import scala.actors._
object SeriousActor extends Actor {
  def act(){
    for(i <- 1 to 5){
      println("別のアクターですが何か?")
      Thread.sleep(1000)
    }
  }
}

それでは実行してみますね

// 2つのアクターを同時に実行してやります
scala> SillyActor.start; SeriousActor.start

// SillyActorとSeriousActorの出力が同時に行われました
// 別々のスレッドで行われているみたいです
scala> 別のアクターですが何か?
俺がアクターです
別のアクターですが何か?
俺がアクターです
別のアクターですが何か?
俺がアクターです
別のアクターですが何か?
俺がアクターです
別のアクターですが何か?
俺がアクターです

動作をイメージするために、例えばSeriousアクターを次のように2秒間隔で実行するようにしてみます

import scala.actors._
object SeriousActor extends Actor {
  def act(){
    for(i <- 1 to 5){
      println("別のアクターですが何か?")
      Thread.sleep(2000)
    }
  }
}

//// 実行してみます
scala> SillyActor.start; SeriousActor.start

// SillyActorが1秒間隔でSeriousActorが2秒間隔で実行されました(`・ω・´)
俺がアクターです

scala> 別のアクターですが何か?
俺がアクターです
俺がアクターです
別のアクターですが何か?
俺がアクターです
俺がアクターです
別のアクターですが何か?
別のアクターですが何か?
別のアクターですが何か? 

また、アクターはscala.actors.Actorオブジェクトのactorというユーティリティメソッドでも作れるみたいです

import scala.actors._
// ブロックで囲まれたアクションを実行するアクターを定義しますね
scala> val seriousActor2 = Actor.actor {
     |   for(i <- 1 to 5){
     |     println("これもアクターです")
     |     Thread.sleep(1000)
     |   }
     | }
seriousActor2: scala.actors.Actor = scala.actors.Actor$$anon$1@408893

// 実行されました
scala> これもアクターです
これもアクターです
これもアクターです
これもアクターです
これもアクターです

actorメソッドを使うとブロックで囲まれたアクションを実行するアクターを生成、かつstartメソッド無しで実行してくれるみたいです。うん、これは便利

アクター同士のメッセージ交換

上の例ではメッセージを外に向けて投げるだけでしたが、本来のアクターはアクター同士のメッセージのやりとりがあってこそ、ということでメッセージの交換を試してみます。

とりあえずメッセージの送信は次のように!メソッドを使うみたいですね

scala> SillyActor ! "おれはここです"

ただし上記のようにSillyActorにメッセージを送っても、SillyActorは受信したメッセージをよむ術がないので何も起こりませんです(´・ω・`)

それでは送ったメッセージを受け取るアクターをつくってみます。メッセージを受け取る場合は、receiveというメソッドを無限ループで実行することでメッセージボックス内のメッセージを読み出すみたいです。

scala> val echoActor = Actor.actor {
     |   while(true){
     |     Actor.receive{
     |       // receiveの引数は次のような部分関数になりますデス
     |       case msg => println("受け取ったメッセージ: "  + msg)
     |     }
     |   }
     | }
echoActor: scala.actors.Actor = scala.actors.Actor$$anon$1@13f5a2f

// echoActorに向けてメッセージ送信しますよ
scala> echoActor ! "Hi Echo"
// 無事に受け取りました
受け取ったメッセージ: Hi Echo

おお!出来ました!(`・ω・´)ちなみにreceiveの引数となる部分関数は15章でもやったとおりマッチの選択肢(「ケース」の連続)として表現されマスネ。なので全ての入力値に対して定義されるわけではなく、部分関数に渡される引数が予め定義された条件とマッチしない限りは処理されないことになるみたいです。まあ、上の例だと全ての入力値を条件としているのでなんでも受け取って処理してしまうわけですが(´・ω・`)…

具体的に部分関数は次のような流れで処理を行うみたいです。

  • isDefinedAtメソッドによってその部分関数で処理できるかを判定
  • isDefinedAtメソッドがtrueを返した場合は部分関数を適用するapplyメソッドを実行

なので、アクターでもきちんとした条件を指定することで、部分関数にマッチしたメッセージのみを処理するってう事ができるわけですね。例えば次のようにIntのメッセージのみを処理するアクターを考えてみマス

import scala.actors.Actor._
val intActor = actor {
  receive{
    case x:Int => println("メッセージは " + x)
  }
}

//// 実行結果ですよ
// 文字列では処理しません
scala> intActor ! "文字列だ"
// 小数も無視します
scala> intActor ! 0.12345
// 整数はきちんと処理しますねー
scala> intActor ! 15     
メッセージは 15
面白くなってきたので脱線してみる

せっかく部分関数リテラルなのでマッチ処理を複数定義してみますよ

import scala.actors.Actor._
val multiActor = actor {
  receive { 
      case x:Int => println("メッセージは整数の " + x)
      case x:Double => println("メッセージは小数の " + x)
      case x:String => println("メッセージは文字列の " + x)
      case x => println("メッセージはなにかしらの " + x)
  }
}

予想では各メッセージの種類によって処理結果が変わるはず...が

// Intは出力されました
scala> multiActor ! 15
メッセージは整数の 15

// Stringは、あれ?
scala> multiActor ! "String Message"
// Doubleも、あれ?
scala> multiActor ! 0.12345         
// こいつもアレレ?
scala> multiActor ! None 

うーん、複数のケース(ケースシーケンス)だとダメ?という疑問が出たものの、とりあえず順番を変えて実行してみると

// 最初だけはいける
scala> multiActor ! "String Message"
メッセージは文字列の String Message

// 2番目移行はダメ
scala> multiActor ! 15

scala> multiActor ! 0.12345

scala> multiActor ! None   
// 同じ型でもダメ
scala> multiActor ! "String Actor" 

(´ε`;)ウーン…とりあえずアクターでのレシーブは1回限りの使い捨てっぽいですな(´・ω・`)でも複数ケース対応自体はできている…と。使い捨ての理由については今後やっていくことになる気がするので、とりあえず先に進みますかねー

いじょうー

とりあえず時間切れなので、ココマデです。次回はネイティブスレッドのアクター利用からやっていきますねー