Reading — step 1 of 7
Learn
Ruby has multiple concurrency primitives. Each has different trade-offs and use cases. The Pickaxe and Programming Ruby treat them across separate chapters.
Threads — preemptive, but GIL-limited
threads = []
5.times do |i|
threads << Thread.new(i) do |n|
sleep(rand * 0.1)
puts "thread #{n}"
end
end
threads.each(&:join)
MRI Ruby has the GIL (Global Interpreter Lock) — only one thread runs Ruby code at a time. CPU-bound work doesn't speed up with threads; I/O-bound work DOES, because the GIL releases during I/O.
JRuby and TruffleRuby don't have a GIL — true parallelism for them.
Fibers — cooperative, lightweight
fib = Fiber.new do
a, b = 0, 1
loop do
Fiber.yield a
a, b = b, a + b
end
end
10.times { puts fib.resume }
Fibers SUSPEND at Fiber.yield and resume at the next .resume. Cooperative — they only yield when they explicitly choose to.
Key properties:
- Tiny stack (~4 KB) — millions of fibers possible
- No locking needed (cooperative)
- Used to implement generators, async I/O
Generators with Enumerator
Fibers power Ruby's Enumerator:
squares = Enumerator.new do |y|
i = 1
loop { y << i*i; i += 1 }
end
squares.next # 1
squares.next # 4
squares.first(5) # [1, 4, 9, 16, 25]
Lazy infinite sequences. The block runs cooperatively, suspending after each y << value.
Async I/O with the fiber scheduler (Ruby 3+)
require 'async'
Async do
3.times.map do |i|
Async do
sleep(0.1)
puts "task #{i} done"
end
end.each(&:wait)
end
The async gem wraps fibers + a scheduler. Concurrent I/O without explicit thread management. Comparable to JavaScript's async/await pattern.
Ractors (Ruby 3+) — true parallelism
worker = Ractor.new do
while msg = Ractor.receive
Ractor.yield msg.upcase
end
end
worker.send("hello")
puts worker.take # "HELLO"
worker.send("world")
puts worker.take # "WORLD"
Ractors are truly parallel — each runs independently, communicating only via message passing. Like Erlang processes, Go goroutines, Rust + threads with channels.
Constraints:
- No shared mutable state — values shared between Ractors must be deeply frozen or shareable
- Many existing libraries don't yet work in Ractors
- Still considered experimental as of Ruby 3.x
Process-based parallelism
pids = []
4.times do |i|
pids << fork do
# in child process — separate memory, separate GIL
perform_work(i)
end
end
Process.waitall
Classic Unix fork. Each child has its own memory. Used by Sidekiq, Resque (background workers), Unicorn, Puma (cluster mode web servers).
When to use what
- Threads — I/O-bound work (HTTP requests, DB queries) — GIL doesn't hurt because threads sleep during I/O
- Fibers / async gem — many concurrent I/O operations on a single thread; lighter than threads
- Ractors — CPU-parallel work in pure Ruby (when supported); experimental
- Processes — heavy CPU work; classic background-job pattern
For most web app code, threads + connection pooling is fine. Reach for fibers/ractors when you've hit a real bottleneck.
Common mistakes
- Expecting CPU speedup from threads in MRI Ruby — GIL prevents it.
- Sharing mutable state between threads without locks — race conditions.
- Forgetting
Thread.abort_on_exception = truein dev — silent thread crashes. - Using
forkin a multi-threaded process — cleanup is fragile; only the forking thread survives in the child.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…