やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

組込ライブラリ(Fiber)

 他言語でいうコルーチン。ファイバーと読む。

成果物

情報源

Fiber

ノンプリエンプティブな軽量スレッド(以下ファイバーと呼ぶ)を提供します。他の言語では coroutine あるいは semicoroutine と呼ばれることもあります。 Thread と違いユーザレベルスレッドとして実装されています。

 よくわからんがスレッドの弱小版みたいなものか?

 コルーチンについてはググった。

サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。

 yieldで一旦返してから再開するやつか。

Thread クラスが表すスレッドと違い、明示的に指定しない限りファイバーのコンテキストは切り替わりません。またファイバーは親子関係を持ちます。Fiber#resume を呼んだファイバーが親になり呼ばれたファイバーが子になります。親子関係を壊すような遷移(例えば自分の親の親のファイバーへ切り替えるような処理)はできません。例外 FiberError が発生します。できることは

  • Fiber#resume により子へコンテキストを切り替える
  • Fiber.yield により親へコンテキストを切り替える

の二通りです。この親子関係は一時的なものであり親ファイバーへコンテキストを切り替えた時点で解消されます。

 yieldなら使うけどresumeは知らん。

ファイバーが終了するとその親にコンテキストが切り替わります。

 そこはサブルーチンと同じ。

なお標準添付ライブラリ fiber を require することにより、コンテキストの切り替えに制限のない Fiber#transfer が使えるようになります。任意のファイバーにコンテキストを切り替えることができます。

 それで何が嬉しいの?

例外

ファイバー実行中に例外が発生した場合、親ファイバーに例外が伝播します。

例:

f = Fiber.new do
  raise StandardError, "hoge"
end

begin
  f.resume     # ここでも StandardError が発生する。
rescue => e
  p e.message  #=> "hoge"
end

ショートチュートリアル

ファイバーは処理のあるポイントで他のルーチンにコンテキストを切り替え、またそのポイントから再開するという目的のために使います。 Fiber.new により与えられたブロックとともにファイバーを生成します。生成したファイバーに対して Fiber#resume を呼ぶことによりコンテキストを切り替えます。子ファイバーのブロック中で Fiber.yield を呼ぶと親にコンテキストを切り替えます。 Fiber.yield の引数が、親での Fiber#resume の返り値になります。

f = Fiber.new do
  n = 0
  loop do
    Fiber.yield(n)
    n += 1
  end
end

5.times do
 p f.resume
end

#=> 0
    1
    2
    3
    4

 Enumratorと似たようなものでは? 何が違うのかよくわからん。

以下は内部イテレータを外部イテレータに変換する例です。実際 Enumerator は Fiber を用いて実装されています。

def enum2gen(enum)
  Fiber.new do
    enum.each{|i|
      Fiber.yield(i)
    }
  end
end

g = enum2gen(1..100)

p g.resume  #=> 1
p g.resume  #=> 2
p g.resume  #=> 3

 ああ、そうなんだ。EnumeratorはFiberで実装されているのね。FiberはEnumeratorよりも低レイヤAPIということか。

注意

Thread クラスが表すスレッド間をまたがるファイバーの切り替えはできません。例外 FiberError が発生します。

f = nil
Thread.new do
  f = Fiber.new{}
end.join
f.resume
#=> t.rb:5:in `resume': fiber called across threads (FiberError)

 だいたいスレッドを使うと危険になるよね。むしろスレッド安全なライブラリって存在するの?

メンバ抜粋

特異メソッド

new yield

インスタンスメソッド

raise resume

new

new {|obj| ... } -> Fiber

与えられたブロックとともにファイバーを生成して返します。ブロックは Fiber#resume に与えられた引数をその引数として実行されます。

ブロックが終了した場合は親にコンテキストが切り替わります。その時ブロックの評価値が返されます。

a = nil
f = Fiber.new do |obj|
  a = obj
  :hoge
end

b = f.resume(:foo)
p a  #=> :foo
p b  #=> :hoge

 yieldを使わないとファイバー感ゼロだね。

yield

yield(*arg = nil) -> object

現在のファイバーの親にコンテキストを切り替えます。

 よく使うやつ。

コンテキストの切り替えの際に Fiber#resume に与えられた引数を yield メソッドは返します。

a = nil
f = Fiber.new do
  a = Fiber.yield()
end

f.resume()
f.resume(:foo)

p a  #=> :foo

 resumeと表裏一体で使うっぽい。

raise

raise -> object
raise(message) -> object
raise(exception, message = nil, backtrace = nil) -> object

selfが表すファイバーが最後に Fiber.yield を呼んだ場所で例外を発生させます。

 ふつうにraiseするのとは違うのかな?

Fiber.yield が呼ばれていないかファイバーがすでに終了している場合、 FiberError が発生します。

 タイミング、場所が違うのかな?

引数を渡さない場合、RuntimeError が発生します。 message 引数を渡した場合、message 引数をメッセージとした RuntimeError が発生します。

 ふーん。

その他のケースでは、最初の引数は Exception か Exception のインスタンスを返す exception メソッドを持ったオブジェクトである必要があります。この場合、2つ目の引数に例外のメッセージを渡せます。また3つ目の引数に例外発生時のスタックトレースを指定できます。

f = Fiber.new { Fiber.yield }
f.resume
f.raise "Error!" # => Error! (RuntimeError)

ファイバー内のイテレーションを終了させる例

f = Fiber.new do
  loop do
    Fiber.yield(:loop)
  end
  :exit
end

p f.resume              # => :loop
p f.raise StopIteration # => :exit

 例外発生させてストップさせるの? 違和感すごい。プログラムは正常終了するし。

resume

resume(*arg = nil) -> object

自身が表すファイバーへコンテキストを切り替えます。自身は resume を呼んだファイバーの子となります。

 引数も渡せる。

ただし、Fiber#transfer を呼び出した後に resume を呼び出す事はできません。

f = Fiber.new do
  Fiber.yield(:hoge)
  :fuga
end

p f.resume() #=> :hoge
p f.resume() #=> :fuga
p f.resume() #=> FiberError: dead fiber called

 transferは親子関係を無視して任意のファイバーへ遷移できるから?

所感

 Array > Enumrator > Fiber。子孫のArrayさえ知っていれば大体OK。遅延評価したければEnumratorを使う。Fiberはたぶん遅延評価したいライブラリを作るときとかに便利なんじゃない? Enumratorよりも自由度が高いのだろう。

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux