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 = true

28.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" → 2
require "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
end

28.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) }