Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
151: Ractor for Parallel Execution Revisited
By now, you've seen the basics of Ractors, but if you've actually tried to use them in a real project, you've probably hit a wall. Ractors aren't just "threads that actually run in parallel"—they are a completely different mental model because of how they handle memory. Let's dive into the parts that usually trip people up when they try to move from theory to production code.
Why does Ruby keep throwing "isolation errors" when I pass a simple object?
This is the number one frustration. In a standard Ruby thread, everything is shared. In a Ractor, almost nothing is. If you try to pass a mutable string or a hash into a Ractor, Ruby will scream at you because if two Ractors could change the same object at the same time, we'd be right back to the thread-safety nightmares that the GVL was designed to prevent.
To fix this, you have two choices: move the object (which makes it inaccessible to the sender) or make it shareable. I usually lean towards Ractor.make_shareable for configuration data. It recursively freezes the object and all its children.
# This will fail
config = { timeout: 30, retries: 3 }
r = Ractor.new do
puts "Timeout is #{Ractor.receive[:timeout]}"
end
r.send(config) # Error: can't pass non-shareable object
# This works
config = { timeout: 30, retries: 3 }
Ractor.make_shareable(config)
r = Ractor.new do
# Since config is shareable, we can even access it via closure
# if it was defined in the outer scope before Ractor creation
puts "Timeout is #{config[:timeout]}"
end
r.take
How do I actually coordinate a pool of Ractors to process a big list of tasks?
Creating a Ractor for every single small task is expensive. You'll spend more time on overhead than on actual computation. The pattern I recommend is a "Worker Pool" where you spin up a fixed number of Ractors (usually matching your CPU core count) and feed them messages.
In this example, imagine we're doing some heavy CPU work, like calculating primes or processing large chunks of text. We can use Ractor.select to grab results as they finish, rather than waiting for them in a strict sequence.
# A simple worker pool for heavy computation
workers = 4.times.map do |i|
Ractor.new(i) do |id|
loop do
# Wait for a number to process
num = Ractor.receive
# Simulate heavy CPU work
result = (1..num).reduce(:+)
Ractor.yield "Worker #{id} finished #{num}: #{result}"
end
end
end
# Feed the workers some jobs
[100_000, 200_000, 300_000, 400_000].each_with_index do |job, i|
workers[i % 4].send(job)
end
# Collect results as they become available
4.times do
# Ractor.select returns [ractor, value]
_r, message = Ractor.select(*workers)
puts message
end
When should I just give up on Ractors and go back to Threads?
I get asked this a lot. Here is my rule of thumb: if your bottleneck is I/O (waiting for an API response, reading from a database, or writing to a disk), do not use Ractors. Ruby's standard Threads are perfectly fine for I/O because the GVL is released during those blocking operations anyway. Ractors add a lot of complexity regarding data isolation that you just don't need for I/O.
Use Ractors only when you have "CPU-bound" work—things like image processing, heavy mathematical calculations, or parsing massive JSON files where you can actually see your CPU usage sitting at 10% while your program crawls. If you're just hitting an endpoint 50 times, stick to Thread.new or a gem like Async.
📋 Practical Task
Exercise: Building a Parallel Log File Analyzer
You have a set of large log strings (simulating files) and you need to count the occurrences of the word "ERROR" across all of them in parallel. Because this is a CPU-intensive string scanning operation, it's a great candidate for Ractors.
Requirements:
- Create a list of at least 5 large strings containing varying amounts of the word "ERROR".
- Spin up 3 worker Ractors.
- Distribute the strings among the workers using
send. - Each worker should return the count of "ERROR" found in its assigned string.
- Use
Ractor.selectorRactor#taketo aggregate the final total count in the main thread. - Ensure you handle the data isolation correctly so that the workers can process the strings without throwing an
IsolationError.
There are no comments for now.