LoginSignup
77
43

More than 3 years have passed since last update.

【Ruby】Fiber(ファイバー)を理解する

Last updated at Posted at 2018-07-24

Fiber(ファイバー)とは?

FiberクラスはThread(スレッド)クラスと似ており、マルチタスクを処理する為に使用されるクラスです。
Fiberを利用することで複数のプログラム間で実行の中断や再開を相互に行わせることができます。

--- Thread(スレッド)に関してはこちらにまとめました。 ---
【Ruby】Thread(スレッド)を理解する

Thread(スレッド)とFiber(ファイバー)の違い

コンピュータのマルチタスクのスケージューリング方式には大きく分けてPreemptive(プリエンプティブ)Nonpreemptive(ノンプリエンプティブ)の二種類があります。

  • Preemptive(プリエンプティブ)方式のスケジューリングではOS(あるいはVM)に処理の切り替えを任せます。

  • Nonpreemptive(ノンプリエンプティブ)方式のスケジューリングではプログラマーが明示的に実行を指示することで処理の切り替えを行います。

Threadクラスが上記のPreemptive(プリエンプティブ)に相当し、FiberクラスがNonpreemptive(ノンプリエンプティブ)に相当します。

つまり、ThreadではOS(VM)に処理の切り替えを任せる一方で、Fiberを使用する事でプログラマ自身が処理のタイミングをコントロールすることができ、「処理を途中で止め、また好きなタイミングで続きを実行する」のようなことが可能となります。

Fiber(ファイバー)の基本用法

ファイバーはFiber.newによって作成する事が可能です。
スレッドと同じで作成時にブロックを渡さないとエラーが発生します。

fiber = Fiber.new do
  'Hello'
end

作成されたFiberインスタンスはFiber#resumeメソッドを持ち、これを呼び出すとブロックが実行されます。

fiber = Fiber.new do
  'Hello'
end
fiber.resume #->'Hello' #ファイバーの持つブロックの実行
#`Fiber#resume`が返り値として'Hello'を返すのではなく、`fiber`に渡されたブロックを`Fiber#resume`によって実行している。

また、ブロック内でFiber.yieldを実行すると処理を停止し、処理を(親ファイバー)に切り替えます。

fiber = Fiber.new do
  p 'Hello' 
  Fiber.yield #-> 処理を停止し親ファイバーに戻る。
  p 'Hello2'
end

fiber.resume #-> 'Hello' #ファイバーの持つブロックの実行

実はFiberには親子関係が存在し、Fiber#resume を実行すると、子ファイバーにコンテキストを切り替え、ブロック中でFiber.yieldした時点でまた親にコンテキストを切り替えます。

fiber = Fiber.new do #Fiberインスタンスの作成。
  p 'Hello' 
  Fiber.yield #=> ②処理を停止し親ファイバーに戻る。
  p 'Hello2'
end

fiber.resume #-> 'Hello' #①ブロック内のFiber.yieldまでを実行
p 'Hello3' #-> 'Hello3` #③ブロック内の処理がFiber.yieldによって停止されたのでメインの処理を継続して実行。
fiber.resume #-> 'Hello2' #④再びFiber#resumeが実行されたので前回処理を停止した位置から処理を再開する。

コード中でも示しました通り、
①ファイバーの持つブロックの実行。(子ファイバー)
②処理を停止し親ファイバーに戻る。(子ファイバーから親ファイバーに切り替え)
③ブロック内の処理がFiber.yieldによって停止されたのでメインの処理を継続して実行。(親ファイバー)
④再びFiber#resumeが実行されたので前回処理を停止した位置から処理を再開する。(子ファイバー)
を順番に行っています。

このように子ファイバーと親ファイバーを利用してキャッチボールのように処理を切り替える事ができるのがFiber(ファイバー)の特徴であり、基本用法となります。

Fiber.yieldFiber#resumeに引数を渡す。

Fiber.yieldに引数が渡された場合

Fiber.yieldに引数を渡す事でその引数をコンテキストの切り替えと共に親ファイバーに渡す事が可能です。
親ファイバーでFiber#resumeが実行されコンテキストの切り替える際にFiber.yieldに与えられた引数を返します。

fiber = Fiber.new do
  Fiber.yield('Hello from fiber')
end

p fiber.resume #=> "Hello from fiber"

ブロックの終了まで実行した場合はブロックの評価結果 を返します。

fiber = Fiber.new do
  p 'Hello'
  Fiber.yield
  p 'Hello2'

  "bye"
end

result = fiber.resume #=> "Hello"
p result #=> nil

result = fiber.resume #=> "Hello2"
p result #=> "bye"

Fiber#resumeに引数が渡された場合

Fiber#resumeも同様に引数が渡された場合にはその引数を子ファイバーに渡す事が可能です。

fiber = Fiber.new do
  p 'Hello'
  p Fiber.yield
end

fiber.resume #=> "Hello"
fiber.resume('Fiber') #=> "Fiber"

Fiber.yieldFiber#resumeの処理の詳細

コメント欄で指摘頂きましたので、Fiber.yieldによって一時停止された処理をFiber.resumeによって処理を再開する際の順序の確認を行います。
以下のサンプルコード①が処理を中断するまで、サンプルコード②が停止された処理を再開する例です。

サンプルコード①

fiber = Fiber.new do #子ファイバー
  p 'Hello'
  p Fiber.yield ##処理を一時停止し親ファイバーに戻す。(Fiber.yieldの評価の開始)
  p 'Hello3'
end

fiber.resume #=> "Hello"
p 'Hello2' #=> 'Hello2'

上記のコード上では子ファイバー内のFiber.yieldで処理が停止した状態です。
ここで親ファイバーから再びFiber#resumeを実行します。
この場合に子ファイバーはFiber.yieldの後から処理を再開するのではなく、まず一時停止されていたFiber.yieldの評価を完了します。

fiber = Fiber.new do #子ファイバー
  p 'Hello'
  p Fiber.yield ##処理を一時停止し親ファイバーに戻す。(※Fiber.yieldの評価の開始)
  p 'Hello3'
end

fiber.resume #=> "Hello"
p 'Hello2' #=> 'Hello2'
fiber.resume('Fiber') #=> Fiber Hello3  #停止した処理の再開(※Fiber.yieldの評価の完了)

# `Fiber.yield`の特徴からブロックの終了まで実行されている。 

上記の例ではFiber.yieldの特徴からブロックの最後まで処理を完結していますので、サンプルコード③Fiber#resumeの呼び出しによってFiber.yieldの評価が完了されていることをより明確に確認します。

サンプルコード③

fiber = Fiber.new do #子ファイバー
  p 'Hello'
  p Fiber.yield ##処理の一時停止(※Fiber.yieldの評価の開始)
  Fiber.yield ##処理の一時停止
  p 'Hello3'
end

fiber.resume #=> "Hello"
p 'Hello2' #=> 'Hello2'
fiber.resume('Fiber') #=> Fiber #停止した処理の再開(※Fiber.yieldの評価の完了)

2度目のFiber.resumeで引数で渡した'Fiber'が表示されている事からFiber#resumeによっていきなり Fiber.yield の次から実行が再開されるののではなく、Fiber.yieldの評価が完了されている事がわかります。

Fiber使用上の注意点

Fiberを使用する際には以下の点に注意する必要があります。
fiberをresumeできるのは「yieldの数+1回」であり、さらにresumeしようとするとFiberErrorが発生します。

fiber = Fiber.new do
  p 'Hello'
  Fiber.yield
  p 'Hello2'
end

fiber.resume #-> 'Hello' #ブロック中のFiber.yieldまでを実行
fiber.resume #-> 'Hello2' #再び子ファイバーに戻り、残りの処理を実行。
fiber.resume #-> dead fiber called (FiberError)

参考

Rubyリファレンス
RubyDocs
Fiberによる協調的な並行プログラミング

77
43
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
77
43