Ruby の Fiber

Fiber は結構複雑なのです。

Fiber.yield がなければ、Fiber は resume で呼び出される proc オブジェクトみたいなもの。ただし、1回しか呼び出せない。

f = Fiber.new do |st|
  st.upcase
end

p f.resume("tokyo")    #"TOKYO"
p f.resume("tokyo")    #a.rb:6:in `resume': dead fiber called (FiberError)

 
ブロック内(子)に Fiber.yield があるとそこで一旦処理が止まる。もう一度(親で)resume されると再開される。

f = Fiber.new do |st|
  puts "Kyoto"
  Fiber.yield
  st.upcase
end

f.resume("tokyo")    #"Kyoto"
p f.resume           #"TOKYO"

 
Fiber.yield で止まったあと、次の resume に引数があればそれが Fiber.yield の返り値になる。

f = Fiber.new do |st|
  puts "Kyoto"
  puts Fiber.yield
  st.upcase
end

f.resume("tokyo")      #"Kyoto"
p f.resume("Osaka")    #"Osaka", "TOKYO"      

 
ブロック変数がない場合は、最初の resume に引数があればそれは捨てられる。

f = Fiber.new do
  puts Fiber.yield
end
  
f.resume("tokyo")  
f.resume("kyoto")    #"Kyoto"

 
Fiber.yield に引数があれば、resume の返り値になる。

f = Fiber.new do |st|
  puts "Kyoto"
  puts Fiber.yield("Nagoya")
  st.upcase
end

p f.resume("tokyo")    #"Kyoto", "Nagoya"
p f.resume("Osaka")    #"Osaka", "TOKYO"

 
ブロック内(子)と外(親)で、断続的に停止しながら通信できるのである。子で止まるときに親に通信できるし、親が再開させるときに子に通信できる。
 

例1

途中で初期化されるフィボナッチ数列

f = Fiber.new do
  a, b = 0, 1
  loop do
    a, b = b, a + b
    x = Fiber.yield(a)
    a, b = 0, 1 if x
  end
end

ar = []
6.times {ar << f.resume(false)}
ar << f.resume(true)
6.times {ar << f.resume(false)}
p ar    #=>[1, 1, 2, 3, 5, 8, 1, 1, 2, 3, 5, 8, 13]

 

例2

20180917080700
Fiber の方では一文字を 0.5秒間隔で表示し、改行があれば文字色を変える。
メインの方は勝手なところで背景色を変えている。両者は独立して動いている。

f = Fiber.new do
  st = "\n"
  color = 0
  loop do
    st += Fiber.yield.to_s
    next if st.empty?
    chr, st = st[0], st[1..-1]
    if chr == "\n"
      print "\e[1;#{(31 + color % 7)}m"    #文字色を変更
      color += 1
    end
    print chr
    sleep(0.5)
  end
end

f.resume    #Fiberの開始

f.resume("Ruby\n")
2.times {f.resume}
print "\e[44m"    #背景色を変更
f.resume("Fiber\n")
5.times {f.resume}
print "\e[45m"    #背景色を変更
f.resume("Coroutine\n")
100.times{f.resume}

 
この場合、Thread でも書けないことはないけれど、同期を取るのが却ってむずかしい。

q = Queue.new

Thread.new do
  color = 0
  st = "\n"
  loop do
    st += q.pop
    until st.empty?
      chr, st = st[0], st[1..-1]
      if chr == "\n"
        print "\e[1;#{(31 + color % 7)}m"
        color += 1
      end
      print chr
      sleep(0.5)
    end  
  end
end

q.push("Ruby\n")
sleep(1.5)
print "\e[44m"
q.push("Fiber\n")
sleep(3)
print "\e[45m"
q.push("Coroutine\n")
sleep(10)

背景色を変えるタイミングもむずかしいし、終了前に余分な sleep を入れないようにするのも面倒。