Posted at

Ruby: Thread と Fiber

More than 3 years have passed since last update.

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


Thread

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


t1.rb

th = Thread.new { 3.times { puts "hello" ; sleep 1 } }

th.join # メインスレッドが終了しないように子スレッドを待つ


$ ruby t1.rb 

hello # 子スレッドで表示
hello #
hello #


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


t2.rb

q = Queue.new

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

3.times { puts q.pop }


$ ruby t2.rb 

hello # メインスレッドで表示
hello #
hello #


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


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 }


$ ruby t3.rb 

hello(1) # メインスレッドで表示 (以下、同じ)
hello(2)
hello(1)
hello(1)
hello(2)
hello(1)
hello(1)
hello(1)
hello(2)
hello(1)


timeout

標準添付ライブラリの timeout はスレッドで実装されています。

以下は、TCP接続のタイムアウトを検出する例です。


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 がスレッド割り込みを行えるようにしています。

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


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回繰り返し呼び出す例


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 メソッドでファイバにコンテキストを切り替え


$ ruby t5.rb 

--> child
hello
hello
--> main
--> child
hello
hello
--> main
--> child
hello
hello
--> main


おわりに

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


  • Ruby 2.1.5 p273