おととい、マルチスレッド下でのメッセージ送信を直列化する(それぞれのオブジェクトがキューを持つ)ようなやつを書いた(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])
おとといのと合わせてアクターごっこしてみたけど、アクターはモデルで考え方なので、これをそのままフレームワークやライブラリにするのは、本当に便利なのかなあ、と思いました。というかわかってたけど書いてみた!みんな思え!