他言語でいうコルーチン。ファイバーと読む。
成果物
情報源
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よりも自由度が高いのだろう。
対象環境
- Raspbierry pi 4 Model B
- Raspberry Pi OS buster 10.0 2020-08-20 ※
- bash 5.0.3(1)-release
- Ruby 3.0.2
$ uname -a Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux