Ruby: Thread と Fiber

Ruby のスレッドとファイバの簡単なメモです。

- [class Thread (Ruby 2.1.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.1.0/class/Thread.html)
- [class Fiber (Ruby 2.1.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.1.0/class/Fiber.html)
- [library fiber (Ruby 2.1.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.1.0/library/fiber.html)

# Thread
子スレッドで"hello"を3回表示する例

```rb:t1.rb
th = Thread.new { 3.times { puts "hello" ; sleep 1 } }

th.join       # メインスレッドが終了しないように子スレッドを待つ
```
```shell-session
$ ruby t1.rb 
hello             # 子スレッドで表示
hello             #     〃
hello             #     〃
```

![thread1.png](https://qiita-image-store.s3.amazonaws.com/0/60624/b415195d-a3f5-6e55-7943-9de7f527dbc7.png)

---
子スレッドからQueue経由で受け取った"hello"をメインスレッドで3回表示する例

```rb:t2.rb
q = Queue.new

th = Thread.new { loop { q << "hello" ; sleep 1 } }

3.times { puts q.pop }
```
```shell-session
$ ruby t2.rb 
hello             # メインスレッドで表示
hello             #       〃
hello             #       〃
```

![thread2.png](https://qiita-image-store.s3.amazonaws.com/0/60624/9a4b3ead-7aa3-5897-ca80-58508102f12f.png)

---
2つの子スレッドからQueue経由で受け取った"hello(*)"をメインスレッドで10回表示する例

```rb:t3.rb
q = Queue.new
m = Mutex.new              # Mutex で Queue を排他ロックする
class << m
  alias call synchronize   # ロックは synchronize でかけるが 
end                        # 名前が長いのでここでは call に alias した

th1 = Thread.new { loop { m.() { q << "hello(1)" } ; sleep 1 } }
th2 = Thread.new { loop { m.() { q << "hello(2)" } ; sleep 3 } }

10.times { puts q.pop }
```

```shell-session
$ ruby t3.rb 
hello(1)       # メインスレッドで表示 (以下、同じ)
hello(2)       
hello(1)
hello(1)
hello(2)
hello(1)
hello(1)
hello(1)
hello(2)
hello(1)
```

![thread3.png](https://qiita-image-store.s3.amazonaws.com/0/60624/267d884c-a59a-bd8d-4a32-a1d3bc189516.png)

# timeout
標準添付ライブラリの timeout はスレッドで実装されています。
以下は、TCP接続のタイムアウトを検出する例です。

```rb:t4.rb
require 'socket'
require 'resolv-replace'   # この require により、Ruby が割り込みを行えるように
                           # DNS名前解決に resolv ライブラリを使うようにする
require 'timeout'

begin
  sock = timeout(10) { TCPSocket.open 'localhost', 80 }
rescue TimeoutError => e    # タイムアウトした場合 TimeoutError 例外があがる
  $stderr.puts e
end

p sock
```

socket ライブラリは DNS 名前解決に C 言語実装のリゾルバを使用しています。
C 言語実装のリゾルバでは、Ruby がスレッド割り込みできません。
`require 'resolv-replace'` することで、C 言語実装のリゾルバの代わりに Ruby の resolv ライブラリを使用します。
そのことで Ruby がスレッド割り込みを行えるようにしています。

- [library resolv-replace (Ruby 2.1.0 リファレンスマニュアル)](http://docs.ruby-lang.org/ja/2.1.0/library/resolv=2dreplace.html)

上の例を簡易に書いた場合

```rb:t4a.rb
require 'socket'
require 'resolv-replace'
require 'timeout'
                   # タイムアウトも含めコネクトできない場合はすべて sock を nil にする
sock = timeout(10) { TCPSocket.open 'localhost', 80 } rescue nil

p sock
```


# Fiber
"hello"を2回表示するファイバ(コルーチン)を3回繰り返し呼び出す例

```rb:t5.rb
require 'fiber'    # Fiber#transfer 等を使用する場合に require が必要。
                   # この例では、この require はなくてよい

f = Fiber.new do
  loop do
    2.times { puts "hello" ; sleep 1 }

    puts "--> main" ; Fiber.yield        # Fiber.#yield で元のファイバにコンテキストを戻す
  end
end

3.times { puts "--> child" ; f.resume }  # resume メソッドでファイバにコンテキストを切り替え
```
```shell-session
$ ruby t5.rb 
--> child
hello
hello
--> main
--> child
hello
hello
--> main
--> child
hello
hello
--> main
```

![fiber1.png](https://qiita-image-store.s3.amazonaws.com/0/60624/b39d1fd6-9e92-d596-852e-25c7892020e4.png)

# おわりに
本稿内容の動作確認は以下の環境で行っています。

- Ruby 2.1.5 p273