Learn Ruby's revolutionary actor-based concurrency model that provides true parallel execution while maintaining memory safety and eliminating data races.
Ractors (Ruby Actors) are Ruby 3.0's answer to true parallel computing. Unlike threads which are limited by the Global VM Lock (GVL), Ractors provide isolated execution contexts that can run in parallel across multiple CPU cores. Each Ractor has its own memory space and communicates with other Ractors through message passing, similar to the Actor model used in languages like Erlang.
class RactorBasics
def self.demonstrate_creation
puts "Main Ractor: #{Ractor.current}"
# Create a simple Ractor
worker = Ractor.new do
puts "Worker Ractor: #{Ractor.current}"
"Hello from worker!"
end
# Wait for result
result = worker.take
puts "Result: #{result}"
# Create Ractor with parameters
calculator = Ractor.new(10, 20) do |a, b|
puts "Calculator Ractor processing: #{a} + #{b}"
a + b
end
sum = calculator.take
puts "Sum: #{sum}"
end
def self.demonstrate_multiple_ractors
# Create multiple workers
workers = 5.times.map do |i|
Ractor.new(i) do |worker_id|
# Simulate work
sleep(rand(0.1..0.5))
"Worker #{worker_id} completed"
end
end
# Collect results
results = workers.map(&:take)
puts "All results: #{results}"
end
end
RactorBasics.demonstrate_creation
RactorBasics.demonstrate_multiple_ractors
class RactorMessaging
def self.demonstrate_bidirectional_communication
# Create a worker that processes messages
processor = Ractor.new do
loop do
message = Ractor.receive
case message[:type]
when :process
result = message[:data].upcase
Ractor.yield(result)
when :exit
break
end
end
end
# Send messages to the processor
processor.send({type: :process, data: "hello world"})
result1 = processor.take
puts "Processed: #{result1}"
processor.send({type: :process, data: "ruby ractors"})
result2 = processor.take
puts "Processed: #{result2}"
# Cleanup
processor.send({type: :exit})
end
def self.demonstrate_select_pattern
# Create multiple message sources
sources = 3.times.map do |i|
Ractor.new(i) do |id|
sleep(rand(0.1..0.3))
Ractor.yield("Message from source #{id}")
end
end
# Use select to handle messages as they arrive
received_count = 0
while received_count < sources.length
r, message = Ractor.select(*sources)
puts "Received: #{message} from #{r}"
received_count += 1
end
end
end
RactorMessaging.demonstrate_bidirectional_communication
RactorMessaging.demonstrate_select_pattern
class RactorWorkerPool
def initialize(pool_size = 4)
@pool_size = pool_size
@workers = []
@task_queue = []
@results = {}
@next_task_id = 0
create_workers
end
def submit_task(&block)
task_id = @next_task_id
@next_task_id += 1
@task_queue << { id: task_id, block: block }
distribute_tasks
task_id
end
def get_result(task_id, timeout: 5)
start_time = Time.now
while Time.now - start_time < timeout
return @results.delete(task_id) if @results.key?(task_id)
collect_results
sleep(0.01)
end
raise "Task #{task_id} timeout"
end
def shutdown
@workers.each { |worker| worker.send({ type: :shutdown }) }
@workers.clear
end
private
def create_workers
@pool_size.times do |i|
worker = Ractor.new(i) do |worker_id|
loop do
message = Ractor.receive
case message[:type]
when :task
begin
result = message[:block].call
Ractor.yield({
type: :result,
task_id: message[:task_id],
result: result,
worker_id: worker_id
})
rescue => e
Ractor.yield({
type: :error,
task_id: message[:task_id],
error: e.message,
worker_id: worker_id
})
end
when :shutdown
break
end
end
end
@workers << worker
end
end
def distribute_tasks
while !@task_queue.empty? && available_worker = find_available_worker
task = @task_queue.shift
available_worker.send({
type: :task,
task_id: task[:id],
block: task[:block]
})
end
end
def find_available_worker
@workers.find { |worker| !worker.instance_variable_get(:@busy) }
end
def collect_results
@workers.each do |worker|
begin
result = worker.take(0.001) # Non-blocking take
case result[:type]
when :result, :error
@results[result[:task_id]] = result
end
rescue Ractor::ClosedError, Ractor::EmptyError
# No message available
end
end
end
end
# Usage example
pool = RactorWorkerPool.new(3)
# Submit CPU-intensive tasks
task_ids = []
5.times do |i|
task_id = pool.submit_task do
# Simulate CPU-intensive work
sum = 0
1_000_000.times { |n| sum += n }
"Task #{i} completed with sum: #{sum}"
end
task_ids << task_id
end
# Collect results
task_ids.each do |task_id|
result = pool.get_result(task_id)
puts "Task #{task_id}: #{result[:result]}"
end
pool.shutdown
class RactorPipeline
def initialize(stages)
@stages = stages
@ractors = []
@input_ractor = nil
@output_ractor = nil
build_pipeline
end
def process(data)
@input_ractor.send(data)
@output_ractor.take
end
def process_batch(batch)
results = []
# Send all data
batch.each { |data| @input_ractor.send(data) }
# Collect results
batch.size.times { results << @output_ractor.take }
results
end
def shutdown
@ractors.each { |ractor| ractor.send(:shutdown) }
end
private
def build_pipeline
previous_ractor = nil
@stages.each_with_index do |stage_proc, index|
ractor = if index == 0
# First stage - receives external input
Ractor.new(stage_proc) do |proc|
loop do
input = Ractor.receive
break if input == :shutdown
result = proc.call(input)
Ractor.yield(result)
end
end
else
# Intermediate/final stages
Ractor.new(previous_ractor, stage_proc) do |input_ractor, proc|
loop do
begin
input = input_ractor.take
result = proc.call(input)
Ractor.yield(result)
rescue Ractor::ClosedError
break
end
end
end
end
@ractors << ractor
@input_ractor = ractor if index == 0
@output_ractor = ractor
previous_ractor = ractor
end
end
end
# Create a data processing pipeline
stages = [
->(data) { data.to_s.upcase }, # Stage 1: Convert to uppercase
->(data) { data.reverse }, # Stage 2: Reverse string
->(data) { "Processed: #{data}" } # Stage 3: Add prefix
]
pipeline = RactorPipeline.new(stages)
# Process single items
puts pipeline.process("hello")
puts pipeline.process("world")
# Process batch
batch_results = pipeline.process_batch(["ruby", "ractors", "parallel"])
puts "Batch results: #{batch_results}"
pipeline.shutdown
class SafeRactorCommunication
def self.demonstrate_safe_sharing
# Shareable objects (safe to pass)
safe_data = {
number: 42,
string: "hello".freeze,
symbol: :test,
array: [1, 2, 3].freeze
}.freeze
processor = Ractor.new do
data = Ractor.receive
puts "Received safe data: #{data}"
# Process and return new frozen data
result = {
processed: data[:string].upcase,
doubled: data[:number] * 2,
count: data[:array].size
}.freeze
Ractor.yield(result)
end
processor.send(safe_data)
result = processor.take
puts "Processing result: #{result}"
end
def self.demonstrate_unsafe_sharing
# This would raise an error
begin
unsafe_data = { mutable_array: [1, 2, 3] } # Not frozen
processor = Ractor.new do
Ractor.receive
end
processor.send(unsafe_data) # This will raise an error
rescue => e
puts "Error sharing unsafe data: #{e.message}"
end
end
end
SafeRactorCommunication.demonstrate_safe_sharing
SafeRactorCommunication.demonstrate_unsafe_sharing
Watch and learn ractors (ruby 3.0+)