LoginSignup
12
9

More than 5 years have passed since last update.

ツナでもわかるmruby [6回目:コルーチン・非同期I/O]

Last updated at Posted at 2014-05-07

mrubyにおける並列・平行処理

mrubyには、標準で提供されるプロセスやスレッドは無いようです。そもそもmrubyは組込み向けに開発されている訳で、OSの機能としてのプロセスやスレッドを利用する事を前提にしないのは当然っちゃ当然ですね。

ただしmrbgemsには幾つか有用なライブラリが提供されていて、それらを使わせて貰えばコルーチンや非同期I/Oが実現できます。

コルーチン(Fiber)

mrubyにおいてもRuby1.9で導入されたFiberがmrbgemsとして提供されています。Fiber、僕はRubyでも直接使った事がないのであまり知らないのですが、mrubyでは有力な平行処理の選択肢の一つとなりますのでここらで勉強します...

Fiberはスレッドとは違い、明示的に切り替えない限りコンテキストは切り替わりません。
つまり一度に走る処理は常に一つであり、コンテキスト切り替えのタイミングは完全に使用者側で制御可能な為、複数のコンテキストから同時に1つのオブジェクトに変更をかけるような事態になる事が避けられ、従って排他制御の必要がありません。

Fiberサンプル1

もっとも簡単なFiber使用例です。

fiber-sample.rb
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-sample2.rb
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に以下を追加

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サーバーを書いてみます。

tcp-server.rb
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クライアントのサンプルを参考に以下のようなコードを書きました。

tcp-client.rb
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つなのですが、サーバーとクライアントでコンテキストを切り替えながら実行するので並列っぽく動きます。

tcp-server-client.rb
# サーバー
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
...
12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9