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


Scalaコップ本の30章をやっていきますよー。30章はアクターを利用した並行プログラミングで、今回からアクタープログラミングの大規模サンプルをやっていきますよー

大規模なサンプル:並列離散イベントシミュレーション


アクタープログラミングのサンプルとして18章でも扱った離散イベントシミュレーションを並列化して、シミュレーションに参加するオブジェクトを独自アクターとして実行出来るようにしてみますよ。並列化することによって複数のプロセッサーを使ってシミュレーションを高速化できるようにしますねー

全体設計

18章の離散イベントシミュレーションでは次のように回路シミュレーションとそのイベントを定義しておりました(´・ω・`)

  • イベント
    • 指定されたタイミングで発生
    • イベント処理によって新しいイベントがスケジューリング
  • 回路シミュレーション
    • ゲートと配線を要素とする
    • 配線の変化をイベントとする離散イベントシミュレーションとして実装

今回からはこれらを並列実行できるように変更していきますデス。具体的にはシミュレーションに含まれるオブジェクトをアクターにすることみたいですね。なので上の定義を元にアクター活動を考えるとこんな感じになるみたいデス(`・ω・´)

  • イベント
    • アクターがイベントを処理
    • イベント状態の大半もアクターが管理
  • 回路シミュレーション
    • ゲートの出力の更新はゲートに対応するアクターによって処理

またシミュレートされる各オブジェクトに共通の振る舞いについてはSimulantというトレイトで吸収してやることにしますデス。またSimulantトレイトをActorとして定義してやることで、各オブジェクトに同時にアクターとしての性質を付与してやりますねー

import scala.actors._
trait Simulant extends Actor

// 例えば配線オブジェクトにも共通の振る舞いをミックスインです
class Wire extends Simulant

とりあえずここらへんまではサクサク決められるのですが、いくつかの課題があるみたいです。

まず課題の1つとしては、シミュレーション上の時間に対してオブジェクトの同期が取れているようにすることがあるみたいです。

同期が取れずに先回りして実行されてしまうイベントがあったりすると(順序を変更されたせいで)処理されないメッセージが出てくるという不具合につながるので、ここでは「タイムチックn-1のイベントを処理し終えるまでタイムチックnのイベントを処理するオブジェクトが現れないようにする」というアプローチを取ることにしますデス(`・ω・´)

このアプローチを実現するためにはオブジェクトが先に進んでもよいかどうかの判定を行う方法を考える必要があるみたいですが、まずは現在時刻の維持管理をする「クロック」アクターを用意して配線やゲートなどの各部品オブジェクトに先に進むタイミングを通知するという素直な実装を考えてみます(´・ω・`)

「クロック」アクターが 全てのオブジェクトが先に進める状態になるまでクロックを停止するために行う具体的な動作は次のようになりますデス

  • 事前に定義されたタイミングで各アクターにPingメッセージを送信
  • 現在のタイムチックに対応するメッセージを受信して処理状況を確認
    • クロックを先に動かせる状態になったオブジェクトがPongメッセージを送信

上記を実現するためにクロック→各オブジェクトに送信されるPingオブジェクトと各オブジェクト→クロックに送信されるPongメッセージを次のように定義します

// クロック→各オブジェクトに送信
case class Ping(time:Int)
// 各オブジェクト→クロックに送信
case class Pong(time:Int, from:Actor)

上記は、Pingメッセージのtimeフィールドを利用してPingとPongのヒモ付を行い、かつPongのfromフィールドで送信元を確認できるようにフィールドの付与によって冗長化してあります。なおPingにfromがないのは送信元が「クロック」アクターひとつだけになるからですね(´・ω・`)これらのデータはデバッグなんかに使うようです

上記のようなメッセージを利用して各部品オブジェクトが現在のタイムチックに関する処理が終わったことを知るには、次の2つの制約条件が必要になるみたいです。

  • オブジェクトは互いに直接メッセージを送信しないで、互いのイベントのスケジューリングだけを行う
  • オブジェクトは現在のタイムチックで処理すべきイベントをポストせず、少なくとも1チック後に実行されるイベントをポストする

上記は結構大きな制約条件になるのですが、普通のシミュレーションであれば許容できるものだそうです。また、システム内の2つのコンポーネントがやり取りする場合は、情報の伝播にある程度の遅れが出るのは当然だし、タイムチックを調整すれば短い間隔の表現も可能で、さらに将来必要になる情報を予め送っておくことも出来るみたいです。

もしもこれらの制約条件を設定しないで部品オブジェクト同士が直接メッセージを送れるようにすると、各アクター間のメッセージには大量の補助情報が必要になるし各アクターがPongメッセージの送信タイミングを判断するメカニズムはもっと高度になってしまうみたいです…まあ、不可能ではないみたいですが。とりあえず今回は素直な方法でやりましょーということで(´・ω・`)

…ということで作業アイテムの予定表はクロックアクターによって管理されるひとつだけにすることになりマス。これによってクロックは現在のタイムチックに対応する全ての作業項目のリクエストを送信し終えるまでPingの送信を待てる→部品オブジェクトのアクターがPingを受け取ったときはタイムチックに発生する作業項目を受け取ったと考えられるみたいです。

上記の判断によって、Pingを受け取ったアクターにはこれ以上の作業項目は送られない→即座にPongを返すことが出来るので、Clockは次の状態を管理すれば良いことになるみたいです。

class Clock extends Actor {
  // シミュレーションの稼働状況
  private var running = false
  // 現在のタイムチック
  private var currentTime = 0
  // 作業項目一覧
  private var agenda:List[WorkItem] = List()
}

最後に解決しておくべき課題として、シミュレーションのセットアップの方法を考える必要があるみたいです。

例えば、通常であれば次のようにシミュレーションを行いマス(`・ω・´)

  • クロックを止めた状態でシミュレーションを作成
  • 要素となる部品オブジェクトを追加
  • 各部品オブジェクトを連結
  • クロックの動作を開始(シミュレーション開始)

上記のようにシミュレーションを行うためには全ての部品オブジェクトが繋げられているという開始可能かどうかの判定が必要になりますが、今回はシミュレーションのセットアップにはメソッド呼び出しのみを利用し、アクターのメッセージがシミュレーション中にしか送らないというアプローチで実現しますです。これによって(規定の)最後のメソッド呼び出しが制御を返した後にはシミュレーションは完全に組みあがっていると見なせるわけです。

これによって次のようなコーディングパターンが決定しますデス

  • シミュレーションセットアップ
    • 通常のメソッド呼び出しを使用
  • シミュレーション中やりとり
    • アクターのメッセージを利用

とりあえず問題になりそうな所を決めてみたので、残りの項目をザザッと決めていきますよー

まずは作業項目としてのWorkItemですが、18 章で定義した実行タイムチック:timeと実行内容のactionのの形式を、具体的にはactionの内容を次のように分割定義してやりますデス

// 作業項目が行うアクションとして
// 作業を行うActorとソレに対するメッセージを定義します
case class WorkItem(time:Int, msg:Any, target:Actor)

また、18章で定義した新しい作業アイテムをスケジューリングするafterDelayメソッドは、クロックから送信されるafterDelayメッセージとして定義してやりますです。内容的には各部品オブジェクトからクロックアクターに送信されるメッセージですかね(´・ω・`)

// WorkItemと同様の形式でメッセージとして定義します
case class AfterDelay(delay:Int, msg:Any, target:Actor)

各オブジェクト間の通信は上記で追加される内容によって間接的にやり取りされるわけですな(`・ω・´)

最後にシミュレーションの開始・終了メソッドを次のように定義しますネ

case object Start
case Object Stop

とりあえず下準備としてはこんな感じですかねー、あとはコレを元に実装していく形になりますね。正直噛み砕けていないところが幾つかありますが、詳細は実装を見ていくことで解消する方向で(`・ω・´)

最後にこの全体設計でやったことをざざっと羅列してみますねー

  • 現在のタイムチックと予定表を管理するClockアクターを定義
  • Clockはすべての部品オブジェクトにpingを送ってみて、全てが準備完了状態になるまでタイムチックを先に進めない
  • 部品オブジェクトのためにはSimulantトレイトが定義されて、ソレゾレがアクターとして動作
  • 各部品オブジェクトはClockの予定表に作業アイテムを追加するという形で他のオブジェクトと通信

いじょー

時間切れでここまでです(´・ω・`)今回もほぼ写経でしたが、次回から上記を基にした具体的な実装に入っていきますねー。なかなか進みませんが、頑張っていきたいと思いますー