mrubyにおける並列・平行処理
mrubyには、標準で提供されるプロセスやスレッドは無いようです。そもそもmrubyは組込み向けに開発されている訳で、OSの機能としてのプロセスやスレッドを利用する事を前提にしないのは当然っちゃ当然ですね。
ただしmrbgemsには幾つか有用なライブラリが提供されていて、それらを使わせて貰えばコルーチンや非同期I/Oが実現できます。
コルーチン(Fiber)
mrubyにおいてもRuby1.9で導入されたFiberがmrbgemsとして提供されています。Fiber、僕はRubyでも直接使った事がないのであまり知らないのですが、mrubyでは有力な平行処理の選択肢の一つとなりますのでここらで勉強します...
Fiberはスレッドとは違い、明示的に切り替えない限りコンテキストは切り替わりません。
つまり一度に走る処理は常に一つであり、コンテキスト切り替えのタイミングは完全に使用者側で制御可能な為、複数のコンテキストから同時に1つのオブジェクトに変更をかけるような事態になる事が避けられ、従って排他制御の必要がありません。
Fiberサンプル1
もっとも簡単なFiber使用例です。
fiber = Fiber.new{ # ①Fiber生成
counter = 0 # ②counter変数を初期化
loop do
puts "top of loop"
Fiber.yield(counter) # ③コンテキストを親へ戻す ⑦コンテキストを親へ戻す
counter += 1 # ⑥counter変数に1を足す
end
}
# ④コンテキストが子から戻る
5.times do
count = fiber.resume # ⑤コンテキストを子へ切り替える
# ⑧コンテキストが子から戻り、返り値をcount変数に代入
p count # ⑨子から受け取った値を表示
end
上のサンプルを実行すると、
mruby fiber-sample.rb
top of loop
0
top of loop
1
top of loop
2
top of loop
3
top of loop
4
コンテキストが親と子の間を行ったり来たりしているのがわかります。
Fiberサンプル2
今度はもう少し複雑な処理です。resume
に引数を渡すと、子のyield
の返り値として受け取られます。これを利用して、親側から子のcounterに自由に値を引いたり足したりしてみます。
fiber = Fiber.new{ # ①Fiber生成
counter = 0 # ②counter初期化
loop do
name, arg = Fiber.yield(counter) # ③親へコンテキストを移す
# ⑥name,argと共に子へコンテキストが移る
puts "called: #{name}(#{arg})"
case name # ⑦受け取ったnameによって処理を切り替える
when :+ # ⑧nameが:+の場合counterにargを足す
counter += arg
when :- # ⑨nameが:-の場合counterからargを引く
counter -= arg
when :clear # ⑩nameが:clearの場合はcounterを0で初期化
counter = 0
end
end
}
fiber.resume # ④一回ループを回しておく
5.times do
count = fiber.resume([:+, 2]) # ⑤counterに2を足す
# ⑪結果をcount変数に代入
p count
count = fiber.resume([:-, 1]) # ⑫counterから1を引く
p count
end
count = fiber.resume(:clear)
p count
実行結果は以下の通り
$ mruby fiber-sample2.rb
called: +(2)
2
called: -(1)
1
called: +(2)
3
called: -(1)
2
called: +(2)
4
called: -(1)
3
called: +(2)
5
called: -(1)
4
called: +(2)
6
called: -(1)
5
called: clear()
0
意図した通りに動作しました。コルーチン使った事無いので、こういう使い方で良いのかわかりませんが...
非同期I/O
mruby-uvのビルド
mrubyには、Node.jsで使用されている非同期I/Oライブラリlibuvを使用する為のmrbgemsが提供されています。
ただし標準ではmrubyに含まれませんのでbuild_config.rb
を修正してmrubyをコンパイルし直す必要が有ります。
build_config.rb
に以下を追加
MRuby::Build.new do |conf|
...
conf.gem :github => 'mattn/mruby-uv' #追加
...
end
mruby-uvをビルドするにはlibtoolとautomakeが必要です。
Macでbrewを使っている場合は以下のようにインストール。
$ brew install libtool
$ brew install automake
ビルドして、できあがった実行ファイルをパスの通った所にインストールします。
$ rake
$ sudo cp ./bin/* /usr/local/mruby/bin/
TCPサーバー
それではごく簡単なTCPサーバーを実装してみます。
mruby-uvはexampleに色々なサンプルが置いてありますので、そのコードを参考にさせてもらいます。
TCPサーバーのサンプルを参考に、ものすごく単純なTCPサーバーを書いてみます。
s = UV::TCP.new
s.bind(UV::ip4_addr("0.0.0.0", 6789))
puts "bound to #{s.getsockname}"
s.listen(5) {|x|
return if x != 0
c = s.accept;
c.write "hello world\r\n"
c.close
}
UV::run()
6789番ポートで待機し、クライアントから接続があったらhello world
というデータを送って一方的にソケットをクローズするサーバーです。
$ mruby tcp-server.rb
bound to 0.0.0.0:6789
サーバーを起動したらtelnetで繋いでみます。
$ telnet localhost 6789
Trying 127.0.0.1...
Connected to localhost.
hello world
Connection closed by foreign host.
たしかにhello world
と表示されました。
TCPクライアント
先ほどはtelnetからサーバーに接続してみましたが、今度はmruby版のTCPクライアントから接続してみます。
TCPクライアントのサンプルを参考に以下のようなコードを書きました。
client = UV::TCP.new
client.connect(UV.ip4_addr("127.0.0.1", 6789)) {|x|
if x == 0
client.read_start { |b|
puts b.to_s
}
else
client.close
end
}
UV::run()
サーバーが停止している場合は、サーバーを再度起動
$ mruby tcp-server.rb
bound to 0.0.0.0:6789
その状態でクライアントを実行
$ mruby tcp-client.rb
hello world
お、いいですね!mruby-uvのサンプルには他にも非同期なファイル操作や一定時間おきに発火するタイマーなど、実戦で役立ちそうな機能のサンプルが色々おいてあります。
TCPサーバーとTCPクライアントを1プロセス内で並列っぽく動かす
折角なので一つのmrubyプロセス内でサーバーとクライアントを両方動かしてみます。非同期I/Oなので走っている処理は常に1つなのですが、サーバーとクライアントでコンテキストを切り替えながら実行するので並列っぽく動きます。
# サーバー
server = UV::TCP.new
server.bind(UV::ip4_addr("0.0.0.0", 16789))
puts "bound to #{server.getsockname}"
server.listen(5) { |x|
return if x != 0
c = server.accept;
timer = UV::Timer.new # サーバーから一定間隔でデータを送るためのタイマー
timer.start(1000, 1000) { |x|
begin
c.write "hello world\r\n"
rescue UVError
c.close
timer.stop
c = timer = nil
end
}
}
# クライアント
client = UV::TCP.new
client.connect(UV.ip4_addr("127.0.0.1", 16789)) {|x|
if x == 0
client.read_start { |b|
puts b.to_s
}
else
client.close
end
}
UV::run()
実行結果
$ mruby tcp-server-client.rb
bound to 0.0.0.0:16789
hello world
hello world
hello world
hello world
hello world
hello world
...