Ruby Ractors — Parallel Execution Without the GIL with Ractor Class
In this tutorial, you will learn about Ruby Ractors. We cover key concepts, practical examples, and best practices to help you master this topic.
Ruby Ractors provide true parallel execution without the GIL using isolated actors communicating through message passing with Ractor class, select, and receive.
What You'll Learn
- Creating Ractors with Ractor.new
- Ractor communication: send and receive
- Ractor isolation rules
- Ractor.select for multiplexing
Why It Matters
Ractors bypass the GIL for CPU-bound parallelism. Ruby 3x3 aims for 3x faster Ruby. DodaZIP uses Ractors for parallel file compression and batch processing.
Real-World Use
Parallel data processing, CPU-intensive computations, image processing, parallel test runners, scientific computing.
flowchart LR
A["Ractors"] --> B["Creation"]
B --> C["Message Passing"]
C --> D["Select"]
D --> E["Isolation Rules"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Creating a Ractor
ractor = Ractor.new do
Ractor.yield 42
end
puts ractor.take
# 42
Message Passing
worker = Ractor.new do
while msg = Ractor.receive
result = msg * 2
Ractor.yield result
end
end
worker.send(10)
worker.send(20)
worker.send(30)
puts worker.take
puts worker.take
puts worker.take
# 20
# 40
# 60
Parallel Computation
def expensive_compute(n)
sleep 0.1
n * n
end
ractors = 5.times.map do |i|
Ractor.new(i) do |arg|
expensive_compute(arg)
end
end
results = ractors.map(&:take)
puts results.inspect
# [0, 1, 4, 9, 16] (in parallel)
Ractor Pool
POOL_SIZE = 4
pool = POOL_SIZE.times.map do
Ractor.new do
loop do
job = Ractor.receive
Ractor.yield job * 2
end
end
end
jobs = (1..10).to_a
results = jobs.map do |job|
pool[job % POOL_SIZE].send(job)
end
puts results.map { pool.map(&:take) }.flatten.sort.inspect
Ractor.select
slow = Ractor.new do
sleep 1
"slow result"
end
fast = Ractor.new do
sleep 0.1
"fast result"
end
r, val = Ractor.select(slow, fast)
puts "First: #{val} from #{r}"
# First: fast result from #<Ractor:#2>
Ractor Server
server = Ractor.new do
state = {}
loop do
action = Ractor.receive
case action[:cmd]
when :set
state[action[:key]] = action[:val]
Ractor.yield :ok
when :get
Ractor.yield state[action[:key]]
end
end
end
server.send({ cmd: :set, key: :name, val: "Ruby" })
puts server.take
server.send({ cmd: :get, key: :name })
puts server.take
# ok
# Ruby
Common Mistakes
1. Sharing Mutable Objects
ary = []
Ractor.new { ary << 1 } # Can't share
# Use Ractor.make_shareable(ary) or Marshal.dump
2. Accessing Global State
$global = 42
Ractor.new { puts $global } # Can't access
3. Ractor Receive Blocking
ractor = Ractor.new { Ractor.receive } # Blocks forever if nothing sent
4. Not Handling Ractor::ClosedError
r = Ractor.new { Ractor.yield 42 }
r.close
r.take # Ractor::ClosedError
5. Using Thread Primitives
Ractor.new { Mutex.new } # ok, but Mutex inside Ractor is per-Ractor
Practice Questions
1. How does Ractor achieve parallelism? Each Ractor runs in a separate OS thread without the GIL. They communicate via message passing, not shared state.
2. What is Ractor.make_shareable? Deep-freezes an object so it can be safely shared between Ractors. Raises if the object references non-shareable objects.
3. How does Ractor.select work? Waits for the first Ractor to yield a value. Returns the Ractor and its value. Non-deterministic ordering.
4. What objects are shareable? Immutable objects (integers, symbols, frozen strings), Ractors, and deeply frozen objects.
Challenge: Build a parallel map function using Ractors that applies a block to each element concurrently.
Solution
def parallel_map(array, &block)
ractors = array.map do |elem|
Ractor.new(elem) { |e| block.call(e) }
end
ractors.map(&:take)
end
puts parallel_map([1, 2, 3, 4, 5]) { |n| n * n }.inspect
FAQ
{{< faq question="Do Ractors replace Threads?" >}} No. Ractors for CPU-bound parallelism. Threads for I/O-bound concurrency. They complement each other. A Ractor can contain multiple threads. {{< /faq >}}
{{< faq question="Can I share a database connection across Ractors?" >}} No, connections are not shareable. Each Ractor needs its own connection or use a Connection Pool inside the Ractor. {{< /faq >}}
{{< faq question="What is the performance benefit?" >}} CPU-bound operations can see near-linear speedup up to the number of CPU cores. I/O-bound tasks may not benefit significantly. {{< /faq >}}
{{< faq question="Can Ractors nest?" >}} Yes, a Ractor can create other Ractors. Parent Ractors can communicate with child Ractors via message passing. {{< /faq >}}
{{< faq question="Is Ractor stable in production?" >}} Ractors are production-ready in Ruby 3.1+. The API is stable but the ecosystem adoption is growing gradually. {{< /faq >}}
Try It Yourself
puts "Starting #{Ractor.count} Ractors..."
r = Ractor.new do
result = (1..1000000).sum
Ractor.yield result
end
puts "Computed: #{r.take}"
Expected output:
Starting 2 Ractors...
Computed: 500000500000
What's Next
Now that you understand Ractors, learn about Ruby Gems for packaging and sharing your code.
| Topic | Description | Link |
|---|---|---|
| Ruby Gems | Creating and publishing gems | {{< ref "43-gems" >}} |
| Ruby Bundler | Dependency management | {{< ref "44-bundler" >}} |
| Go Goroutines | Compare with Go's approach | Go |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro