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


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

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

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

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

###Fiberサンプル1
もっとも簡単なFiber使用例です。

```ruby: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
今度はもう少し複雑な処理です。<code>resume</code>に引数を渡すと、子の<code>yield</code>の返り値として受け取られます。これを利用して、親側から子のcounterに自由に値を引いたり足したりしてみます。

```ruby: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](https://github.com/joyent/libuv)を使用する為のmrbgemsが提供されています。
ただし標準ではmrubyに含まれませんので<code>build_config.rb</code>を修正してmrubyをコンパイルし直す必要が有ります。

<code>build_config.rb</code>に以下を追加

```ruby: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に色々な[サンプル](https://github.com/mattn/mruby-uv/tree/master/example)が置いてありますので、そのコードを参考にさせてもらいます。
[TCPサーバーのサンプル](https://github.com/mattn/mruby-uv/blob/master/example/tcp-server.rb)を参考に、ものすごく単純なTCPサーバーを書いてみます。

```ruby: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番ポートで待機し、クライアントから接続があったら<code>hello world</code>というデータを送って一方的にソケットをクローズするサーバーです。

```
$ 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.
```
たしかに<code>hello world</code>と表示されました。

###TCPクライアント
先ほどはtelnetからサーバーに接続してみましたが、今度はmruby版のTCPクライアントから接続してみます。
[TCPクライアントのサンプル](https://github.com/mattn/mruby-uv/blob/master/example/tcp-client.rb)を参考に以下のようなコードを書きました。

```ruby: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のサンプル](https://github.com/mattn/mruby-uv/tree/master/example)には他にも[非同期なファイル操作](https://github.com/mattn/mruby-uv/blob/master/example/fs.rb)や[一定時間おきに発火するタイマー](https://github.com/mattn/mruby-uv/blob/master/example/loop.rb)など、実戦で役立ちそうな機能のサンプルが色々おいてあります。

###TCPサーバーとTCPクライアントを1プロセス内で並列っぽく動かす

折角なので一つのmrubyプロセス内でサーバーとクライアントを両方動かしてみます。非同期I/Oなので走っている処理は常に１つなのですが、サーバーとクライアントでコンテキストを切り替えながら実行するので並列っぽく動きます。

```ruby: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
...
```