Bird
Raised Fist0
Rubyprogramming~15 mins

Ractor for true parallelism in Ruby - Deep Dive

Choose your learning style10 modes available
Overview - Ractor for true parallelism
What is it?
Ractor is a feature in Ruby that allows programs to run multiple parts at the same time on different CPU cores. It creates isolated units called ractors that do not share memory, so they can truly run in parallel without interfering with each other. This helps Ruby programs use modern computers better by doing many tasks simultaneously. Ractors communicate by sending messages, keeping data safe and avoiding common problems with shared memory.
Why it matters
Without true parallelism, Ruby programs can only do one thing at a time, even on computers with many cores. This limits speed and efficiency, especially for heavy tasks like data processing or web servers. Ractor solves this by letting Ruby use all CPU cores safely and effectively. Without Ractor, Ruby developers had to rely on slower or more complex methods to run tasks in parallel, making programs slower and harder to write.
Where it fits
Before learning Ractor, you should understand Ruby basics like variables, methods, and threads. Knowing about concurrency and the problems with shared memory helps too. After Ractor, you can explore advanced parallel programming, performance tuning, and distributed systems in Ruby.
Mental Model
Core Idea
Ractor is like separate workers in isolated rooms who do tasks at the same time and only talk by passing notes, so they never get in each other's way.
Think of it like...
Imagine a kitchen with many chefs, each in their own room with their own ingredients and tools. They cook different dishes at the same time without bumping into each other. When they need to share a recipe or ingredient, they pass a written note through a small window. This way, everyone works fast and safely without mixing up ingredients or steps.
┌─────────────┐   message   ┌─────────────┐
│  Ractor 1   │───────────▶│  Ractor 2   │
│ (isolated) │   note     │ (isolated)  │
└─────────────┘           └─────────────┘

Each Ractor runs independently on its own CPU core.
No shared memory, only message passing.
Build-Up - 7 Steps
1
FoundationUnderstanding Ruby's concurrency limits
🤔
Concept: Ruby threads run concurrently but not truly in parallel due to a global lock.
Ruby uses a Global Interpreter Lock (GIL) that lets only one thread run Ruby code at a time. This means even if you have many threads, they take turns running, so you don't get real parallelism on multiple CPU cores. Threads share memory, which can cause bugs if not handled carefully.
Result
Ruby threads can switch quickly but do not run Ruby code simultaneously on multiple cores.
Knowing Ruby's GIL limitation explains why true parallelism needs a different approach like Ractor.
2
FoundationIntroducing Ractor: isolated parallel units
🤔
Concept: Ractor creates isolated units that run truly in parallel without sharing memory.
A Ractor is like a separate Ruby interpreter running in parallel. It has its own memory and state, so it cannot accidentally change data in another Ractor. This isolation avoids the problems caused by shared memory in threads.
Result
Ractors can run Ruby code simultaneously on multiple CPU cores safely.
Understanding isolation is key to grasping how Ractor achieves true parallelism.
3
IntermediateCommunicating between Ractors with messages
🤔Before reading on: do you think Ractors share variables directly or communicate differently? Commit to your answer.
Concept: Ractors communicate by sending messages, not by sharing variables.
Since Ractors do not share memory, they send messages to each other using ports. Messages are copied or moved safely between Ractors. This message passing is the only way to exchange data, preventing race conditions and data corruption.
Result
You can coordinate work between Ractors without risking shared memory bugs.
Knowing message passing replaces shared variables helps avoid common concurrency errors.
4
IntermediateCreating and using Ractors in Ruby code
🤔Before reading on: do you think Ractors run code immediately or wait for a start command? Commit to your answer.
Concept: You create a Ractor with a block of code that runs immediately in parallel.
Use Ractor.new { ... } to start a new Ractor. The block runs in parallel with the main program. You can send and receive messages using ractor.send and ractor.take. Ractors run independently until they finish or are stopped.
Result
Your Ruby program runs multiple tasks truly in parallel.
Understanding how to create and communicate with Ractors unlocks practical parallel programming.
5
IntermediateHandling data safely with Ractor's copy-on-send
🤔Before reading on: do you think data sent between Ractors is shared or copied? Commit to your answer.
Concept: Data sent between Ractors is copied or moved to keep isolation intact.
When you send data to another Ractor, Ruby copies it or moves it if possible. This means each Ractor has its own copy, so changes in one do not affect the other. Some objects can be moved instead of copied for efficiency, but the original Ractor loses access.
Result
Data safety is guaranteed without locks or synchronization.
Knowing how data moves between Ractors explains why shared memory bugs disappear.
6
AdvancedAvoiding common pitfalls with Ractor communication
🤔Before reading on: do you think sending complex objects between Ractors always works smoothly? Commit to your answer.
Concept: Not all objects can be sent between Ractors; some raise errors or require special handling.
Certain objects like IO handles, mutexes, or singleton objects cannot be shared or sent. Trying to send them raises errors. You must design your program to send only safe, serializable data. Also, blocking operations inside Ractors can cause deadlocks if not managed carefully.
Result
Your Ractor programs run reliably without unexpected crashes or freezes.
Understanding Ractor's communication limits prevents subtle bugs in parallel programs.
7
ExpertPerformance trade-offs and internal scheduling
🤔Before reading on: do you think Ractors always speed up programs linearly with more cores? Commit to your answer.
Concept: Ractors improve parallelism but have overhead and scheduling complexities that affect performance.
Creating and messaging between Ractors has costs. The Ruby scheduler manages Ractors on OS threads, balancing CPU use. Too many Ractors or heavy message passing can reduce gains. Also, some Ruby internal operations are not fully parallel yet. Profiling and tuning are needed for best results.
Result
You get faster programs but must balance Ractor use carefully.
Knowing Ractor's internal costs helps write efficient parallel Ruby code and avoid performance surprises.
Under the Hood
Ractor runs as a separate Ruby execution context with its own heap and thread. It uses OS threads to run truly in parallel on multiple CPU cores. Memory is not shared; instead, data is copied or moved between Ractors via message queues. The Ruby VM schedules Ractors cooperatively, switching between them and managing system resources. This isolation prevents race conditions and data corruption common in shared-memory concurrency.
Why designed this way?
Ruby's Global Interpreter Lock limited true parallelism, frustrating developers. Ractor was designed to provide safe parallelism without breaking Ruby's simplicity. Isolation avoids complex locking and synchronization bugs. Message passing is a proven model from other languages for safe concurrency. Alternatives like shared memory with locks were rejected due to complexity and error-proneness.
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   Ractor 1    │       │   Ractor 2    │       │   Main Thread │
│  (own heap)  │       │  (own heap)  │       │  (own heap)   │
└──────┬────────┘       └──────┬────────┘       └──────┬────────┘
       │ message queue             │ message queue          │
       │──────────────▶           │◀───────────────        │
       │                          │                        │
       ▼                          ▼                        ▼
  OS Thread 1                OS Thread 2               OS Thread 0

Each Ractor runs isolated with its own memory and OS thread.
Messages pass through queues to communicate safely.
Myth Busters - 4 Common Misconceptions
Quick: Do Ractors share variables directly like threads? Commit yes or no.
Common Belief:Ractors share variables just like threads do, so you can access the same data from multiple Ractors.
Tap to reveal reality
Reality:Ractors do NOT share variables or memory. Each Ractor has its own copy of data, and communication happens only via message passing.
Why it matters:Assuming shared memory leads to bugs and crashes because Ractors cannot see or change each other's data directly.
Quick: Do Ractors always make your Ruby program faster? Commit yes or no.
Common Belief:Using Ractors automatically makes any Ruby program run faster by using all CPU cores.
Tap to reveal reality
Reality:Ractors can improve speed but add overhead. Poor design, too many Ractors, or heavy messaging can slow programs down.
Why it matters:Believing Ractors are a magic speed fix can cause inefficient code and wasted resources.
Quick: Can you send any Ruby object between Ractors without issues? Commit yes or no.
Common Belief:All Ruby objects can be sent between Ractors safely without errors.
Tap to reveal reality
Reality:Some objects like IO, Mutex, or singleton objects cannot be sent and will raise errors if attempted.
Why it matters:Trying to send unsupported objects causes crashes and hard-to-debug errors.
Quick: Does Ractor replace threads completely in Ruby? Commit yes or no.
Common Belief:Ractor is a full replacement for threads and should be used everywhere instead of threads.
Tap to reveal reality
Reality:Ractor complements threads but does not replace them. Threads are still useful for IO-bound tasks and shared-memory concurrency.
Why it matters:Misusing Ractors instead of threads can complicate code unnecessarily or reduce performance.
Expert Zone
1
Ractors cannot share mutable state, but frozen objects can be shared without copying, improving performance subtly.
2
Ruby's internal C extensions may not be fully Ractor-safe, requiring careful testing or alternative implementations.
3
Message passing between Ractors can be optimized by moving objects instead of copying, but this invalidates the sender's access, a subtle trade-off.
When NOT to use
Avoid Ractors when your program heavily relies on shared mutable state or third-party libraries that are not Ractor-safe. For IO-bound concurrency, Ruby threads or async frameworks like Async or EventMachine may be better. Also, for simple parallelism, processes or external job queues can be alternatives.
Production Patterns
In production, Ractors are used to parallelize CPU-heavy tasks like data processing, image manipulation, or background jobs. They are combined with message queues for communication and supervision patterns to restart failed Ractors. Developers often limit the number of Ractors to CPU cores and carefully design message protocols to avoid bottlenecks.
Connections
Actor Model (Computer Science)
Ractor is Ruby's implementation of the Actor Model pattern.
Understanding the Actor Model helps grasp Ractor's isolation and message passing as a proven concurrency design.
Microservices Architecture
Both use isolated units communicating via messages to achieve parallelism and fault tolerance.
Seeing Ractors as microservices inside a program clarifies how isolation and messaging improve reliability and scalability.
Human Teamwork in Separate Rooms
Ractors resemble teams working independently and coordinating only by passing notes.
This cross-domain view highlights how isolation and communication prevent conflicts and improve efficiency.
Common Pitfalls
#1Trying to share mutable objects directly between Ractors.
Wrong approach:shared_array = [] r1 = Ractor.new { shared_array << 1 } r2 = Ractor.new { shared_array << 2 }
Correct approach:r1 = Ractor.new { arr = []; arr << 1; arr } r2 = Ractor.new { arr = []; arr << 2; arr }
Root cause:Misunderstanding that Ractors do not share memory and that mutable objects must be isolated.
#2Sending unsupported objects like IO handles between Ractors.
Wrong approach:r = Ractor.new { |io| io.read } r = Ractor.new { File.open('file.txt') } r.send(File.open('file.txt'))
Correct approach:r = Ractor.new { |path| File.read(path) } r.send('file.txt')
Root cause:Not knowing which objects are safe to send causes runtime errors.
#3Creating too many Ractors without managing resources.
Wrong approach:1000.times { Ractor.new { sleep(10) } }
Correct approach:cores = Etc.nprocessors cores.times { Ractor.new { sleep(10) } }
Root cause:Ignoring system limits and overhead leads to resource exhaustion and poor performance.
Key Takeaways
Ractor provides true parallelism in Ruby by running isolated units that do not share memory.
Communication between Ractors happens only through message passing, avoiding shared-memory bugs.
Not all Ruby objects can be sent between Ractors; understanding safe data transfer is crucial.
Ractors improve performance but require careful design to avoid overhead and communication bottlenecks.
Ractor complements Ruby threads and fits into a broader concurrency and parallelism strategy.

Practice

(1/5)
1. What is the main purpose of using Ractor in Ruby?
easy
A. To run code truly in parallel on multiple CPU cores
B. To create graphical user interfaces
C. To handle database connections
D. To write single-threaded scripts

Solution

  1. Step 1: Understand Ractor's role

    Ractor is designed to enable true parallel execution in Ruby, using multiple CPU cores.
  2. Step 2: Compare options

    Options A, B, and C do not relate to parallelism or CPU core usage.
  3. Final Answer:

    To run code truly in parallel on multiple CPU cores -> Option A
  4. Quick Check:

    Ractor = true parallelism [OK]
Hint: Ractor means real parallel tasks in Ruby [OK]
Common Mistakes:
  • Confusing Ractor with threads
  • Thinking Ractor is for UI or database
  • Assuming Ractor runs code sequentially
2. Which of the following is the correct way to create a new Ractor in Ruby?
easy
A. Ractor.new { puts 'Hello from Ractor' }
B. Thread.new { puts 'Hello from Ractor' }
C. Process.spawn { puts 'Hello from Ractor' }
D. Fiber.new { puts 'Hello from Ractor' }

Solution

  1. Step 1: Identify Ractor creation syntax

    Ractor is created using Ractor.new with a block of code to run in parallel.
  2. Step 2: Eliminate other options

    Thread, Process, and Fiber are different concurrency tools, not Ractor.
  3. Final Answer:

    Ractor.new { puts 'Hello from Ractor' } -> Option A
  4. Quick Check:

    Use Ractor.new to create Ractors [OK]
Hint: Ractor.new starts a parallel task [OK]
Common Mistakes:
  • Using Thread.new instead of Ractor.new
  • Confusing Process.spawn with Ractor
  • Using Fiber.new which is cooperative, not parallel
3. What will be the output of this Ruby code using Ractor?
r = Ractor.new { 5 * 5 }
result = r.take
puts result
medium
A. RactorError
B. 5 * 5
C. nil
D. 25

Solution

  1. Step 1: Understand Ractor block execution

    The block { 5 * 5 } runs inside the Ractor and returns 25.
  2. Step 2: Use take to get the result

    r.take receives the value returned by the Ractor block, which is 25.
  3. Final Answer:

    25 -> Option D
  4. Quick Check:

    Ractor returns 25, take receives it [OK]
Hint: Ractor block returns value, take fetches it [OK]
Common Mistakes:
  • Expecting string '5 * 5' instead of calculation
  • Confusing take with yield
  • Assuming nil or error without communication
4. Identify the error in this Ractor code snippet:
r = Ractor.new do
  Ractor.yield 10
  20
end
puts r.take
puts r.take
medium
A. Ractor.new requires arguments
B. Second r.take call will cause Ractor::Closed error
C. No error, code runs fine
D. Calling Ractor.yield inside Ractor block is invalid

Solution

  1. Step 1: Understand Ractor.yield and r.take

    Ractor.yield sends a value to the main thread, and r.take receives it. The first r.take gets 10.
  2. Step 2: Analyze second r.take call

    The block returns 20 after yield, which is the final value. The second r.take gets 20. After that, the Ractor is closed. A third r.take would cause error, but here only two calls are made, so no error.
  3. Step 3: Check the code carefully

    Both r.take calls are valid; no error occurs.
  4. Final Answer:

    No error, code runs fine -> Option C
  5. Quick Check:

    Ractor.yield and take work twice safely [OK]
Hint: Ractor.yield sends values, take receives them sequentially [OK]
Common Mistakes:
  • Thinking Ractor.yield is invalid inside Ractor
  • Expecting error on second take call
  • Assuming Ractor.new needs arguments
5. You want to sum numbers from 1 to 100 using two Ractors running in parallel, each summing half the range. Which code correctly implements this?
hard
A. r1 = Ractor.new { (1..100).sum } r2 = Ractor.new { (1..100).sum } puts r1.take + r2.take
B. r1 = Ractor.new { (1..50).sum } r2 = Ractor.new { (51..100).sum } puts r1.take + r2.take
C. r1 = Thread.new { (1..50).sum } r2 = Thread.new { (51..100).sum } puts r1.value + r2.value
D. r1 = Ractor.new { (1..50).reduce(:+) } r2 = Ractor.new { (51..100).reduce(:+) } puts r1.take * r2.take

Solution

  1. Step 1: Split the range correctly

    We want two Ractors, one summing 1 to 50, the other 51 to 100.
  2. Step 2: Check the operations and combining results

    r1 = Ractor.new { (1..50).sum } r2 = Ractor.new { (51..100).sum } puts r1.take + r2.take sums each half and adds results correctly. r1 = Ractor.new { (1..100).sum } r2 = Ractor.new { (1..100).sum } puts r1.take + r2.take sums full range twice (wrong). r1 = Thread.new { (1..50).sum } r2 = Thread.new { (51..100).sum } puts r1.value + r2.value uses Thread, not Ractor. r1 = Ractor.new { (1..50).reduce(:+) } r2 = Ractor.new { (51..100).reduce(:+) } puts r1.take * r2.take multiplies sums instead of adding.
  3. Final Answer:

    r1 = Ractor.new { (1..50).sum } r2 = Ractor.new { (51..100).sum } puts r1.take + r2.take -> Option B
  4. Quick Check:

    Split range, sum halves, add results [OK]
Hint: Split work, sum halves in Ractors, then add [OK]
Common Mistakes:
  • Using Thread instead of Ractor
  • Summing full range twice
  • Multiplying sums instead of adding
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard