Io みたいな async を Ruby でやってみる
http://subtech.g.hatena.ne.jp/secondlife/20080605/1212634318 みてふと思いついたのでやってみた。
class Foo
def heavy(n)
sleep n
n
end
end
f = Foo.new
p :a
foo = f.async(:heavy, 2) # 即座にかえる
p :b
p foo # まつ
p :c
p foo # 即座にかえる
p :d
foo = f.async(:heavy, 2)
bar = f.async(:heavy, 5)
p :e
p [foo, bar] #=> 5秒まってから [2, 5]
foo = f.async(:heavy, 2)
bar = f.async(:heavy, 5)
p :f
sleep 5 # 5秒間カレントスレッドをとめてスイッチ
p :g
p [foo, bar] #=> 即 [2, 5]Io の foo @heavy の雰囲気そのままな感じ。
スレッドなのでどこで実行されるかは確実ではない。Ruby のスレッドは IO 待ちとか sleep とかでスイッチする。
実装:
class Future
def initialize(obj, name, args, block)
@obj, @name, @args, @block = obj, name, args, block
@th = Thread.start do
Thread.pass # 一応明示的に pass しておく
@obj.send(@name, *@args, &@block)
end
end
def method_missing(name, *args, &block)
@th.value.send(name, *args, &block)
end
# 全部委譲してなりすます
Object.instance_methods.each do |m|
next if %w|__send__ __id__ object_id|.include? m.to_s
undef_method m
end
end
class Object
def async(name=nil, *args, &block)
Future.new(self, name, args, block)
end
endThread をラップした Future をつくるだけなのでたいしたことはやってない。この方法だとどんなメソッドでも非同期にできる。
undef_method しまくって method_missing に飛ばしているので、パっと見もとのオブジェクトを変わらない Future インスタンスができる。(inspect も書きかえているということなので、p は信じてはいけない)
p は inspect をよんでるし、#{} は to_s をよんでいるので、透過的に扱えるようにみえる。このオブジェクトにメッセージがおくられた結果を使う限り、Future は見えない。
Lazy と似てるけど違うのは、呼ぶのが確定した時点でもう評価を開始するところ (Ruby のスレッドは暇になるまでスイッチしないけど)。最終的に準備ができていないと待たされるけど、準備できていれば待たされない。
あとでもこれだと symbol をわたしてなんかカッコわるいのでもう一個中間オブジェクトをかましてみる。
class Object
def async(name=nil, *args, &block)
name ? Future.new(self, name, args, block) : AsyncObject.new(self)
end
class AsyncObject
def initialize(obj)
@obj = obj
end
def method_missing(name, *args, &block)
Future.new(@obj, name, args, block)
end
Object.instance_methods.each do |m|
next if %w|__send__ __id__ object_id|.include? m.to_s
undef_method m
end
end
endp :a
foo = f.async.heavy(2) # 即座にかえる
p :b
p foo # まつ
p :c
p foo # 即座にかえる
p :d
a = f.async.heavy(1)
b = f.async.heavy(2)
c = f.async.heavy(3)
p :e
sleep 3 # カレントスレッドやすむ
p :f
p [a, b, c] # 即座にかえるアクターモデルについては今調べたりしてみてるけど、まだよくわかってない……
コールバックは instance_eval
p [
Kernel.async.sleep(3).async.instance_eval { p "done #{self}"; self },
Kernel.async.sleep(2).async.instance_eval { p "done #{self}"; self },
Kernel.async.sleep(1).async.instance_eval { p "done #{self}"; self },
]
__END__
"done 1"
"done 2"
"done 3"
[3, 2, 1]最初の定義で、Object#async を Future よりあとに定義していることに注意が必要です。Future#async は method_missing よばれません (よばれるとこういうふうに書けないですね)。
サブテクの日記の最新タイトルをとってきてみる
require "net/http"
require "uri"
def aget(uri)
puts "getting => #{uri}"
Net::HTTP.async.get(uri).async.instance_eval {
p "done #{uri}"
self
}
end
require "rubygems"
require "hpricot"
base = URI("http://subtech.g.hatena.ne.jp/diarylist")
doc = Hpricot(aget(base))
doc.search(".hatena-body .day .refererlist ul li a").map {|a| aget(base + a[:href] + "rss") }.map {|body|
doc = Hpricot(body)
[doc.at("//dc:creator").inner_text, doc.at("//item/title").inner_text]
}.each do |id, title|
puts "id:%s : %s" % [id, title]
end実行してみると getting と done がまざって実行されているのがわかると思う。一旦全部 async しないといけないから map が二回でてきてるのはかっこわるいかも……