#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.yield
+ Fiber#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.yield
+ Fiber#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)