Ractors and Fibers
Prerequisites: Thread safety: Mutex and Queue
Threads cover most concurrency needs in Ruby. Two more tools handle the cases they don't: Ractors for CPU-bound parallelism, and fibers for very large numbers of concurrent I/O tasks.
Ractors — parallel Ruby code
A Ractor ("Ruby actor") is an isolated unit of execution with its own lock, so several Ractors can run Ruby code in parallel on different cores. The price is isolation: Ractors can't share mutable objects. They communicate by sending messages.
ractors = [1..2_000_000, 2_000_001..4_000_000].map do |range|
Ractor.new(range) { |r| r.sum { |n| Math.sqrt(n) }.round }
end
p ractors.map(&:take).sum
sums.rb:2: warning: Ractor is experimental, and the behavior may change in future versions of Ruby! Also there are many implementation issues.
5333334333
- Arguments passed to
Ractor.neware copied (or moved) into the Ractor. The block can't use variables from outside it — everything it needs must be passed in as an argument. takewaits for the Ractor's result. (Newer Ruby versions are reworking this API — Ruby 3.5 and later usevalueinstead oftake— so check the documentation for your version.)- Ractors are still experimental in Ruby 3.4, and many gems aren't Ractor-safe yet.
For production CPU-bound work today, the proven approach is still multiple processes — for example, a job queue with several worker processes, or a pre-forking server like Puma in clustered mode.
Fibers — lightweight cooperative tasks
A fiber is a unit of work that pauses and resumes explicitly, rather than being switched by the system like a thread:
fiber = Fiber.new do
puts "step 1"
Fiber.yield
puts "step 2"
end
fiber.resume
puts "in between"
fiber.resume
step 1
in between
step 2
Fibers are very cheap — you can have tens of thousands. Since Ruby 3.0, a fiber scheduler can pause a fiber automatically whenever it waits for I/O and run another one. You don't usually write a scheduler yourself; the async gem provides one:
require "async"
require "net/http"
Async do |task|
%w[https://example.com https://ruby-lang.org].each do |url|
task.async { puts Net::HTTP.get_response(URI(url)).code }
end
end
Each request runs in its own fiber, and ordinary Net::HTTP code
becomes non-blocking with no changes.
Choosing
| Need | Use |
|---|---|
| A handful of concurrent I/O tasks | Threads |
| Thousands of concurrent connections | Fibers with async |
| CPU-heavy work across cores (production) | Multiple processes |
| CPU-heavy work across cores (experimental) | Ractors |