Question
Does Ruby Have Real Multithreading? MRI Threads, GVL, and CPU Cores
Question
Does Ruby support real operating-system-level threads rather than only cooperative green threads? How can an application use multiple CPU cores for CPU-intensive processing?
Short Answer
You will learn the difference between native OS threads and parallel CPU execution in Ruby. In particular, you will understand why modern MRI Ruby threads are useful for I/O concurrency but are limited by the Global VM Lock (GVL), and when to choose Ractors, processes, or another Ruby implementation for CPU-bound work.
Concept
Modern Ruby implementations can create threads that are scheduled by the operating system. However, having OS-level threads does not always mean Ruby code can execute on multiple CPU cores at the same instant.
In the most common Ruby implementation, MRI (also called CRuby), a mechanism called the Global VM Lock (GVL) allows only one thread to execute Ruby bytecode at a time within one process.
That produces two important outcomes:
- I/O-bound work can benefit from threads. While one thread waits for a network response, file read, database result, or sleep timer, another thread can run.
- CPU-bound Ruby work does not run in parallel through ordinary threads. Two threads calculating large values in pure Ruby still take turns holding the GVL.
Ruby 1.8 historically used green threads in its standard implementation. Modern MRI Ruby uses native threads, but its GVL remains relevant for CPU parallelism.
To use several CPU cores for pure Ruby calculations, common choices are:
- Ractors in Ruby 3+, which provide isolated concurrent execution designed for parallel work.
- Multiple processes, using
Process.forkon supported platforms or process-worker libraries. - Alternative implementations such as JRuby, whose threading model does not have MRI's GVL limitation.
- Native extensions that release the GVL while doing intensive native work, when an appropriate library supports it.
The key distinction is: native threads are about OS scheduling; parallelism is about simultaneously executing work on more than one core.
Mental Model
Think of an MRI Ruby process as a workshop with many workers (threads) but only one key to the machine room (the GVL).
- A worker can hold the key and operate Ruby code.
- If that worker is waiting for a delivery, such as a database or HTTP response, another worker can use the key.
- But two workers cannot operate Ruby machines at the same time because there is only one key.
Ractors are more like separate workshops. Each has its own ability to work, but they must pass carefully packaged messages instead of freely sharing mutable tools.
Syntax and Examples
Create a Ruby thread with Thread.new:
thread = Thread.new do
puts "Running in a separate thread"
42
end
result = thread.value
puts result
Thread.new starts a native Ruby thread. Calling thread.value waits for the thread to finish and returns the block's final value.
Threads are especially appropriate when tasks spend time waiting on I/O:
urls = [
"https://example.com",
"https://example.org"
]
threads = urls.map do |url|
Thread.new do
# Pretend this is an HTTP request.
sleep 1
"Downloaded #{url}"
end
end
puts threads.map(&:value)
Both threads can wait during roughly the same one-second period. In real code, replace sleep with an HTTP, database, or file operation.
For CPU-bound work in Ruby 3+, use a Ractor when the work and data can be isolated:
workers = [10, 20].map do |number|
Ractor.new(number) do |n|
n * n
end
end
results = workers.map(&:take)
p results # [100, 400]
Each Ractor receives an argument and returns a result with take. Ractors do not freely share mutable objects, which helps make parallel execution safer.
Step by Step Execution
Consider this I/O-style threaded example:
first = Thread.new do
sleep 1
"first finished"
end
second = Thread.new do
sleep 1
"second finished"
end
puts first.value
puts second.value
Step by step:
Thread.newcreatesfirst, which begins and immediately callssleep(1).- Because
firstis sleeping, it is not actively executing Ruby instructions. - The main thread creates
second, which also begins and sleeps. - The two one-second waits overlap.
first.valuewaits untilfirsthas completed, then returns"first finished".second.valuereturns"second finished".
The program normally finishes in about one second rather than two because waiting overlaps.
Now replace sleep with a large pure-Ruby calculation. On MRI, ordinary threads may still interleave their work, but only one can execute Ruby bytecode at a time due to the GVL. That means they generally do not provide CPU-speedup proportional to the number of cores.
Real World Use Cases
Threads are a good fit in MRI Ruby when a program has many independent waits:
- Downloading data from several HTTP APIs.
- Reading many files from slow storage or network locations.
- Polling multiple queues or sockets.
- Running background work while a service waits on databases or remote services.
- Maintaining connections in a web server or job worker.
Use processes or Ractors for CPU-heavy work such as:
- Generating image thumbnails or processing large media files, when the underlying tool can use parallelism.
- Computing large reports, simulations, or transformations in pure Ruby.
- Parsing and analyzing independent large datasets.
- Running several independent CPU-intensive jobs from a queue.
The best choice depends on what dominates the runtime: waiting suggests threads; calculation suggests parallel workers.
Real Codebase Usage
In real Ruby applications, threads are usually managed through a bounded worker pool rather than creating an unlimited number of threads.
A common pattern is concurrent I/O with error collection:
urls = ["https://example.com", "https://example.org"]
threads = urls.map do |url|
Thread.new do
begin
# response = Net::HTTP.get(URI(url))
sleep 0.1
[url, :ok]
rescue StandardError => error
[url, error]
end
end
end
results = threads.map(&:value)
Important production practices include:
- Bound concurrency. Hundreds or thousands of threads can exhaust memory, connections, or API rate limits. Use a fixed-size pool or a job queue.
- Always wait for worker results. Use
join,value, or a pool API so failures are not silently ignored. - Handle errors inside workers. An exception in a thread may otherwise be noticed only when you call
value. - Protect shared mutable state. Use
Mutexwhen multiple threads update the same object. - Prefer message passing for CPU workers. Ractors and separate processes reduce accidental shared-state bugs.
Web servers and background-job systems often use multiple processes, each with a limited thread pool, to balance CPU usage, I/O concurrency, and isolation.
Common Mistakes
Assuming Thread.new makes CPU work parallel in MRI
# This does not make pure Ruby calculations run simultaneously on two cores in MRI.
2.times do
Thread.new { 50_000_000.times { 1 + 1 } }
end
Modern MRI threads are native, but the GVL permits only one thread to run Ruby bytecode at a time. Use Ractors, multiple processes, or a suitable native library for CPU-bound parallelism.
Forgetting to wait for threads
Thread.new do
sleep 1
puts "May never be printed"
end
The main program can exit before the worker finishes. Keep the thread and call join or value:
worker = Thread.new { sleep 1; puts "Finished" }
worker.join
Sharing mutable data without synchronization
counter = 0
threads = 10.times.map do
Thread.new do
1_000.times { counter += 1 }
end
end
threads.each(&:join)
The result can be unreliable because counter += 1 is a read-modify-write operation. Protect it with a Mutex:
counter = 0
lock = Mutex.new
threads = 10.times.map do
Thread.new do
1_000.times do
lock.synchronize { counter += 1 }
end
end
end
threads.each(&:join)
Creating one thread per item
Creating thousands of threads for thousands of jobs can overload the machine or remote services. Use a bounded worker pool so only a controlled number of tasks run at once.
Trying to share ordinary mutable objects between Ractors
Ractors intentionally restrict mutable sharing. Design Ractor tasks to receive simple values, immutable data, or messages, then return results.
Comparisons
| Option | Uses OS threads? | Runs pure Ruby CPU work in parallel? | Best for |
|---|---|---|---|
MRI Thread | Yes | Usually no, because of the GVL | I/O concurrency |
MRI Ractor | Uses parallel execution facilities | Yes, when work is isolated | CPU-bound independent tasks |
| Multiple processes | Yes, through separate OS processes | Yes | Strong isolation and CPU workers |
JRuby Thread | Yes | Yes, subject to normal thread-safety concerns | JVM-based Ruby applications |
| Term | Meaning |
|---|---|
| Concurrency | Multiple tasks make progress during the same period, often by taking turns or waiting efficiently. |
| Parallelism | Multiple tasks execute literally at the same time on different CPU cores. |
| GVL | MRI's lock that allows one thread at a time to execute Ruby bytecode in a process. |
| Green thread | A thread scheduled by a language runtime rather than directly by the OS. |
Cheat Sheet
- Create a thread:
Thread.new { work } - Wait for completion:
thread.join - Get result and re-raise worker errors:
thread.value - Modern MRI threads are native OS threads.
- MRI has a GVL, so pure Ruby CPU work in ordinary threads is not parallel.
- Use MRI threads mainly for I/O-bound tasks.
- Use Ractors (Ruby 3+) or processes for independent CPU-bound work.
- Use
Mutex#synchronizewhen threads modify shared mutable state. - Avoid an unbounded number of threads; limit concurrency with a pool.
sleep, network calls, file operations, and database waits can allow other threads to run.- A native extension may release the GVL, but this depends on the extension; do not assume it does.
FAQ
Does Ruby have real OS threads?
Yes. Modern MRI Ruby uses native threads provided by the operating system. Older Ruby 1.8 behavior is the source of many references to green threads.
Why do Ruby threads not use all CPU cores in MRI?
MRI's Global VM Lock allows only one thread at a time to execute Ruby bytecode within one process. Threads can still be useful while other threads wait for I/O.
Can MRI Ruby threads run in parallel during database or HTTP calls?
They can make progress concurrently when calls wait for I/O, provided the library performs blocking work in a way that releases execution for other threads. This improves throughput for waiting-heavy workloads.
What should I use for CPU-bound work in Ruby?
For modern Ruby, consider Ractors for isolated tasks. Multiple processes are also a common and broadly useful approach. Benchmark with realistic work before choosing.
Are Ractors the same as threads?
No. Ractors are isolated execution units with restricted object sharing and message passing. Their isolation is intended to support safer parallel work.
Is Thread#join different from Thread#value?
join waits for a thread and returns the thread object. value waits, returns the block's result, and raises an exception if the thread failed.
Does JRuby have the same GVL limitation as MRI?
No. JRuby uses JVM threads and does not use MRI's GVL model, so Ruby threads can run CPU-bound work in parallel. Thread-safe shared-state design is still necessary.
Mini Project
Description
Build a small parallel calculation tool using Ractors. It calculates the sum of squares for several independent numeric ranges, then combines the partial results. This models a reporting or data-processing job whose input can be split into independent CPU-bound chunks.
Goal
Use multiple Ractors to calculate independent partial totals and combine their results in the main Ractor.
Requirements
Create several independent numeric ranges to process.
Start one Ractor for each range.
Calculate the sum of squares within each Ractor.
Collect every partial result with Ractor#take.
Print the combined total.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.