Fiber Scheduler の理解と並列リクエスト可能な条件
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 1
と sleep 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);
}
あとは Fiber Scheduler の方で、他の Fiber に処理を移しつつ待機する処理を書けば実現できます。
async gem では、 Async::Scheduler#io_wait
に実装があります。
フックできるメソッドのリストは以下のドキュメントに書かれています。
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;
}
逆に言うと、Ruby の IO class に依存している HTTP client などは並列リクエスト可能です。例えば aws-cli は seahorse というモジュールで HTTP リクエストをしていますが、こちらは Ruby で書かれているので並列リクエストできます。
まとめ
Fiber Scheduler で並列リクエスト可能な処理についてまとめました。 gRPC では使えなさそうだったのが残念でしたが、 Fiber についての理解が深まりました。
Discussion