🗂

Fiber Scheduler の理解と並列リクエスト可能な条件

2025/02/23に公開

Fiber や FiberScheduler を使って並列リクエストを送りたかったのでまとめます!以下のような人に向けて書いています。

  • Fiber は軽量スレッドらしいけどよさがわからない
  • Fiber Scheudler ってやつを使うと便利らしいが何がいいのかわからない
  • Fiber Scheduler を使うとなんで並列リクエストが送れるようになるのかわからない

自分も調べる前まではよくわかってなかったのでこの記事で理解していってもらえると嬉しいです!

Fiberとは

Fiberは軽量スレッドであり、Rubyにおける並行処理の一つの手段です。以下は簡単なFiberの例です。

fiber = Fiber.new do
  puts "Hello"
  Fiber.yield
  puts "World"
end

fiber.resume # => "Hello"
fiber.resume # => "World"

この例では、fiber.resumeを呼び出すことでFiberの処理を再開し、Fiber.yieldで一時停止しています。

この例だけを見ると、Fiberで並列処理ができるようには見えません。
プログラマが自前で切り替え処理を書かないと切り替えられないので、リクエストを送ったときにどう切り替えるのかがわかりません。

ここで Fiber Scheduler の出番です。

Fiber Schedulerとは

Fiber Schedulerは、Fiberを使ってnonblocking IOを実現するためのインターフェイスです。Ruby本体では実装の提供はなく、async gem などに実装があります。以下はasync gemを使った例です。

require 'async'

Async do |task|
  task.async do
    puts "Task 1"
    sleep 1
    puts "Task 1 done"
  end

  task.async do
    puts "Task 2"
    sleep 2
    puts "Task 2 done"
  end
end

この例では、task.asyncを使って複数のタスクを並行して実行していますが、Fiber 単体で実行するのとは少し挙動が違います。

Fiber単体なら、この処理は sleep 1sleep 2 があるので全体で約3秒かかりますが、
Fiber Scheduler を使っているので、片方の sleep をしている間にもう片方の sleep も実行でき、トータル約2秒で処理が終わります。

Fiber Scheduler を使うと、IO があるコードは並列処理されているように見えます。ここで念の為並行と並列についておさらいしておきます。

並行と並列のちがい

並行処理と並列処理は似ているようで異なる概念です。

  • 並行処理 (Concurrency):

    • 複数のタスクが進行中である状態を指します。
    • これらのタスクは必ずしも同時に実行されるわけではなく、タスク間で切り替えながら実行されます。
    • 例えば一人のシェフが複数の料理を順番に少しずつ調理する。という具合です。
  • 並列処理 (Parallelism):

    • 複数のタスクが同時に実行される状態を指します。
    • これには複数のプロセッサやコアが必要です。
    • 例: 複数のシェフがそれぞれ別の料理を同時に調理する。

FiberやFiber Schedulerを使うと、Rubyでは並行処理が実現できます。ただ、Fiber Scheduler を使うと、nonblocking IO が実現できるため、IO 中は他の処理が可能です。

そのため、複数のIOを同時にリクエストし、待機するということが可能になります。これをこの記事では「並列リクエスト」と呼んでいます。

Fiber Scheduler でなぜ並列リクエストが実現できるのか

Fiber Schedulerが定義されていると、RubyのIO#readなどの待機処理が必要なメソッドの実装をFiber Schedulerの実装に置き換えられます。以下はその仕組みを示す簡単な例です。

require 'async'
require 'net/http'

Async do |task|
  task.async do
    uri = URI('http://example.com')
    response = Net::HTTP.get(uri)
    puts response
  end

  task.async do
    uri = URI('http://example.org')
    response = Net::HTTP.get(uri)
    puts response
  end
end

この例では、Net::HTTP.getが非同期に実行され、待機中に他のFiberに制御が移ります。

この仕組みは ruby の io.c 内を読むとよくわかります。
Fiber Scheduler が定義されているとき、FiberScheduler の io_wait method に io_wait の処理が切り替わることがわかります。

rb_fiber_scheduler_current で scheduler の実装を取得し、if 文で存在を確認しているのがわかるかと思います。

rb_io_wait(VALUE io, VALUE events, VALUE timeout)
{
    VALUE scheduler = rb_fiber_scheduler_current();

    if (scheduler != Qnil) {
        return rb_fiber_scheduler_io_wait(scheduler, io, events, timeout);
    }

https://github.com/ruby/ruby/blob/06451f0dec107625f3a7794230e90d539a55a08a/io.c#L1449-L1453

あとは Fiber Scheduler の方で、他の Fiber に処理を移しつつ待機する処理を書けば実現できます。
async gem では、 Async::Scheduler#io_waitに実装があります。

https://github.com/socketry/async/blob/b1a0472c1f724dfa96180e174a0c9f3b09e92418/lib/async/scheduler.rb#L290-L303

フックできるメソッドのリストは以下のドキュメントに書かれています。

https://docs.ruby-lang.org/en/3.4/Fiber/Scheduler.html

FiberScheduler を使って並列リクエストを送れる条件は?

結論、FiberSchedulerを使って並列リクエストを送れる条件としては、RubyのIO#readなど、待機処理をFiberScheduler側でフックできるような実装になっている必要があります。

つまり、RubyのIOクラスの実装などを使わず、C拡張を使ってリクエストをしていて、独自の待機処理を書いているような実装では使えません。

例えば、gRPC gemは使えません。

あれはgRPCのcore C実装をwrapしているような実装になっており、待機処理がgRPCのcore C実装内部に書かれているため、FiberSchedulerでhookする場所を書く余地がありません。

以下の grpc_completion_queue_pluck を実行している箇所で gRPC クライアントは結果が返ってくるのを待っており、これは gRPC の core C実装に当たります。

static void* grpc_rb_completion_queue_pluck_no_gil(void* param) {
  next_call_stack* const next_call = (next_call_stack*)param;
  next_call->event = grpc_completion_queue_pluck(next_call->cq, next_call->tag,
                                                 next_call->timeout, NULL);
  return NULL;
}

https://github.com/grpc/grpc/blob/2fbc586e28dc957cabfa5b1c6776eb0f28e0a7c1/src/ruby/ext/grpc/rb_completion_queue.c#L44

逆に言うと、Ruby の IO class に依存している HTTP client などは並列リクエスト可能です。例えば aws-cli は seahorse というモジュールで HTTP リクエストをしていますが、こちらは Ruby で書かれているので並列リクエストできます。

まとめ

Fiber Scheduler で並列リクエスト可能な処理についてまとめました。 gRPC では使えなさそうだったのが残念でしたが、 Fiber についての理解が深まりました。

Discussion