@m_seki の

I like ruby tooから引っ越し

アクターとモデル、Linda

おととい、マルチスレッド下でのメッセージ送信を直列化する(それぞれのオブジェクトがキューを持つ)ようなやつを書いた(http://d.hatena.ne.jp/m_seki/20110718#1310987230)ので、こんどはRindaをエーテルとして使ってみる。先日の例ではメッセージの送信はメソッド呼び出しの中でこっそり行われたけど、今回はわざとらしくメッセージを送ることにする。

タプルはだいたい三種類でやってみる。メッセージをアドレスに送るには次の三つ組のタプルを使う。アドレスはSymbolで、居眠り床屋問題で出てくるアクター :shop と :barber。メッセージもSymbolね。

  [アドレス, メッセージ, Object]
  [:barber, :wake_up, nil]
  [:barber, :customer, 'makoto']

あるアクターの状態を調べるには、次の二つ組のタプルをreadすればよい。

 [アドレス, Object]
 [:barber, 'sleep']

アクターが終了すると、そのアドレスだけの入ったタプルがwriteされる。

  [アドレス]
  [:shop]

https://raw.github.com/seki/MoreRinda/master/example/sleeping_barber_rinda.rb

require 'rinda/tuplespace'
require 'rinda/eval'

class BarberShop
  def initialize(ts)
    @ts = ts
    @waiting = []
  end

  def start
    @ts.write([:shop, @waiting])
    while true
      addr, msg, arg = @ts.take([:shop, nil, nil])
      case msg
      when :new_customer
        name = arg
        puts "<< arriving #{name}"
        if (@waiting.size >= 3)
          puts "** sorry, no room for #{name} **"
        else
          @waiting << name
          @ts.write([:barber, :wake_up, nil])
        end
      when :customer_leaves
        puts "leaving #{@waiting.shift}>>"
        @ts.write([:barber, :customer, @waiting[0]])
      when :barber_check
        @ts.write([:barber, :customer, @waiting[0]])
      when :quit
        return
      end
      @ts.take([:shop, nil])
      @ts.write([:shop, @waiting])
    end
  end
end

class Barber
  def initialize(ts)
    @ts = ts
  end

  def start
    @ts.write([:barber, 'sleep'])
    while true
      addr, msg, arg = @ts.take([:barber, nil, nil])
      case msg
      when :wake_up
        @ts.take([:barber, nil])
        @ts.write([:barber, 'wake up'])
        @ts.write([:shop, :barber_check, nil])
      when :customer
        if arg
          cut(arg)
          @ts.write([:shop, :customer_leaves, nil])
        else
          puts "                     sleeping"
          @ts.take([:barber, nil])
          @ts.write([:barber, 'sleep'])
        end
      when :quit
        return
      end
    end
  end

  def cut(name)
    puts "                     cutting #{name}."
    rand(5).times do
      puts "                        cutting..."
      sleep(1)
    end
    puts "                     finishing cutting #{name}."
  end
end

place = Rinda::TupleSpace.new
DRb.start_service('druby://localhost:0')

Rinda::rinda_eval(place) do |ts|
  BarberShop.new(ts).start
  [:shop]
end

Rinda::rinda_eval(place) do |ts|
  Barber.new(ts).start
  [:barber]
end

%w(jack john henry tom bob m_seki makoto).each do |name|
  place.write([:shop, :new_customer, name])
  sleep(2)
end

p place.read([:shop, []])
p place.read([:barber, 'sleep'])

place.write([:shop, :quit, nil])
place.write([:barber, :quit, nil])

p place.take([:shop])
p place.take([:barber])

rinda_evalを使っているのでMoreRindaを入れる必要がありますが、複数のプロセスによる実験が必要ない(マルチスレッドでよい)ひとはThread.newで書き直せば実験できると思う。

Rinda::rinda_eval(place) do |ts|
  BarberShop.new(ts).start
  [:shop]
end

これは新しいプロセスを生成して、BarbarShopを作って処理をはじめる記述です。startメソッドがおわったとき、そのアクターは終了です。最後の[:shop]はこのアクターが終了したことを通知しています。

Rindaをエーテルとして使うといろんな面倒なことを引き受けてくれます。たとえば、相手のアクターがまだ準備ができていなくても、そのアドレスへの通知はエーテルが覚えておいてくれたりとか、みんなの様子を覗き見たりとか。

なお、ちょーかっこいいのは、終了の待合せの部分です。

p place.read([:shop, []])
p place.read([:barber, 'sleep'])

お客さんの追加が終わったあと、read([:shop, []])してます。これは:shopの状態、@waitingが空になるまでブロックするということです。次にread([:barber, 'sleep'])を待ちます。待ってる人がいなくて、床屋が眠ったら、もうこの遊びは終わり。:shopと:barberに:quitを投げ、それぞれの終了([:shop], [:barber])を待ちます。

place.write([:shop, :quit, nil])
place.write([:barber, :quit, nil])

p place.take([:shop])
p place.take([:barber])

おとといのと合わせてアクターごっこしてみたけど、アクターはモデルで考え方なので、これをそのままフレームワークやライブラリにするのは、本当に便利なのかなあ、と思いました。というかわかってたけど書いてみた!みんな思え!