Concurrency and Parallelism
28.1 The GVL (Global VM Lock)
In CRuby, the GVL lets only one thread execute Ruby code at a time. Threads still run I/O concurrently (the GVL is released during blocking I/O). CPU-bound parallelism needs processes or Ractors.
JRuby and TruffleRuby have no GVL: threads there run truly in parallel.
28.2 Threads
Synchronization: Mutex, Monitor (re-entrant), ConditionVariable, Queue/SizedQueue, Thread#join(timeout).
concurrent-ruby gem: Concurrent::Future, Promises, thread pools, AtomicFixnum, Map, TimerTask, actors.
require "net/http"
urls = %w[https://ruby-lang.org https://rubygems.org https://github.com]
threads = urls.map do |url|
Thread.new(url) do |u|
response = Net::HTTP.get_response(URI(u))
[u, response.code]
end
end
results = threads.map(&:value) # join and get the return values
counter = 0
lock = Mutex.new
10.times.map { Thread.new { 1000.times { lock.synchronize { counter += 1 } } } }.each(&:join)
counter # 10000
q = Queue.new # thread-safe queue (also SizedQueue)
producer = Thread.new { 5.times { q << it }; q << :done }
consumer = Thread.new { while (item = q.pop) != :done do puts item end }
[producer, consumer].each(&:join)
Thread.current[:request_id] = "abc" # fiber-local
Thread.current.thread_variable_set(:x, 1) # thread-local
t = Thread.new { raise "boom" }
t.report_on_exception = false
t.abort_on_exception = true28.3 Fibers and the Fiber Scheduler
Fiber Scheduler (Ruby 3.0+): non-blocking I/O for fibers. The async gem implements it:
Used by Falcon (a fiber-based web server). Ruby 3.3 added an M:N thread scheduler (opt-in: RUBY_MN_THREADS=1).
fiber = Fiber.new do
puts "step 1"
Fiber.yield 1
puts "step 2"
2
end
fiber.resume # "step 1" → 1
fiber.resume # "step 2" → 2require "async"
require "async/http/internet"
Async do |task|
internet = Async::HTTP::Internet.new
results = %w[https://example.com https://ruby-lang.org].map do |url|
task.async { internet.get(url).read.size }
end.map(&:wait)
p results
ensure
internet&.close
end28.4 Ractors (True Parallelism, Experimental)
Ractors don't share mutable objects. They communicate by message passing (copied or moved objects), and shareable objects are frozen/immutable (Ractor.make_shareable).
Still experimental: many gems aren't Ractor-safe yet. Ruby 4.0 reworked the Ractor communication API (Ractor::Port).
ractors = 4.times.map do |i|
Ractor.new(i) do |n|
(1..5_000_000).sum { it * n } # CPU-bound work in parallel
end
end
p ractors.map(&:take) # Ruby 3.x API (`value`/`join` in Ruby 3.5/4.0 with Ractor::Port)28.5 Processes
Web servers use multi-process (Puma cluster mode, Unicorn, Pitchfork) plus threads (Puma).
pid = fork { heavy_work } # Unix only
Process.wait(pid)
# Parallel gem: Parallel.map(items, in_processes: 4) { |i| compute(i) }