2008年 06月 05日

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
end

Thread をラップした 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
end
p :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 が二回でてきてるのはかっこわるいかも……