【Ruby】Fiber（ファイバー）を理解する

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

_--- Thread(スレッド)に関してはこちらにまとめました。 ---_
[【Ruby】Thread(スレッド)を理解する](https://qiita.com/k-penguin-sato/items/1326882c400cac8c109b)

#Thread(スレッド)とFiber(ファイバー)の違い
コンピュータのマルチタスクのスケージューリング方式には大きく分けて__`Preemptive(プリエンプティブ)`__と__`Nonpreemptive(ノンプリエンプティブ)`__の二種類があります。

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

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

[Threadクラス](https://qiita.com/k-penguin-sato/items/1326882c400cac8c109b)が上記の__`Preemptive(プリエンプティブ)`__に相当し、Fiberクラスが__`Nonpreemptive(ノンプリエンプティブ)`__に相当します。

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


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

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

```ruby
fiber = Fiber.new do
  'Hello'
end
```

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

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

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

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

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

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

```ruby
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`に与えられた引数を返します。

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

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

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

```ruby
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`も同様に引数が渡された場合にはその引数を子ファイバーに渡す事が可能です。


```ruby
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`によって処理を再開する際の順序の確認を行います。
以下の__サンプルコード①__が処理を中断するまで、__サンプルコード②__が停止された処理を再開する例です。

__サンプルコード①__

```ruby
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`の評価を完了します。

```ruby
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`の評価が完了されていることをより明確に確認します。

__サンプルコード③__

```ruby
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が発生します。

```ruby 
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リファレンス](https://docs.ruby-lang.org/ja/latest/class/Fiber.html)
[RubyDocs](https://ruby-doc.org/core-2.2.0/Fiber.html)
[Fiberによる協調的な並行プログラミング](https://allabout.co.jp/gm/gc/447245/)