Feature #20876
openIntroduce `Fiber::Scheduler#blocking_operation_wait` to avoid stalling the event loop.
Description
This is an evolution of the previous proposal: https://bugs.ruby-lang.org/issues/20855
Background¶
The current Fiber Scheduler performance can be significantly impacted by blocking operations that cannot be deferred to the event loop, particularly in high-concurrency environments where Fibers rely on non-blocking operations for efficient task execution.
Proposal¶
Pull Request: https://github.com/ruby/ruby/pull/12016
We will introduce a new fiber scheduler hook called blocking_operation_work
:
class MySchduler
# ...
def blocking_operation_wait(work)
# Example (trivial) implementation:
Thread.new(&work).join
end
end
We introduce a new flag for rb_nogvl
: RB_NOGVL_OFFLOAD_SAFE
which indicates that rb_nogvl(func, ...)
is a blocking operation that is safe to execute on a different thread or thread pool (or some other context).
When a C extension invokes rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)
, and a fiber scheduler is available, all the arguments will be saved into a instance of a callable object (at this time a Proc
) called work
and passed to the blocking_operation_wait
fiber scheduler hook. When work
is #call
ed, it will execute rb_nogvl
again with all the same arguments.
The fiber scheduler can decide how to execute that work, e.g. on a separate thread or thread pool, to mitigate the performance impact of the blocking operation on the event loop.
Cancellation¶
rb_nogvl
takes several arguments, a func
for the actual work, and unblock_func
to cancel func
if possible. These arguments are preserved in the work
proc, and cancellation works the same. However, some extra effort may be required in the fiber scheduler hook, e.g.
class MySchduler
# ...
def blocking_operation_wait(work)
thread = Thread.new(&work)
thread.join
thread = nil
ensure
thread&.kill
end
end
Example¶
Using the branch of async
gem: https://github.com/socketry/async/pull/352/files and enabling zlib deflate to use this feature, the following performance improvement was achieved:
require "zlib"
require "async"
require "benchmark"
DATA = Random.new.bytes(1024*1024*100)
duration = Benchmark.measure do
Async do
10.times do
Async do
Zlib.deflate(DATA)
end
end
end
end
# Ruby 3.3.4: ~16 seconds
# Ruby 3.4.0 + PR: ~2 seconds.
To run this benchmark yourself, you must compile CRuby with these two PRs:
In addition, enable RB_NOGVL_OFFLOAD_SAFE
in zlib.c
's call to rb_nogvl
.
Then, use this branch of async: https://github.com/socketry/async/pull/352
Files