Threads and the GVL
Prerequisites: Blocks, HTTP requests and APIs
Concurrency means making progress on several tasks during the same period of time. Parallelism means running several tasks at literally the same instant on different CPU cores. The difference matters a lot in Ruby.
Real-world analogy: one chef cooking three dishes, switching between them while each simmers, is concurrency. Three chefs cooking at once is parallelism.
Threads
def fetch(name, seconds)
sleep(seconds) # stands in for a slow network request
"#{name} done"
end
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
threads = [["users", 1], ["orders", 1], ["stock", 1]].map do |name, secs|
Thread.new { fetch(name, secs) }
end
results = threads.map(&:value)
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
p results
puts "Took about #{elapsed.round} second(s)"
["users done", "orders done", "stock done"]
Took about 1 second(s)
Three one-second tasks finished in about one second, not three.
Thread.new starts the block running in the background; value waits
for the thread to finish and returns the block's result (join waits
without returning it).
The GVL
The standard Ruby interpreter (CRuby, also called MRI) has a Global
VM Lock (GVL, sometimes called the GIL): only one thread runs Ruby
code at a time in a process. But a thread releases the lock while
it waits — for the network, the disk, a database, or sleep.
So in CRuby:
- I/O-bound work (HTTP calls, database queries, file operations) gets real speed-ups from threads, as above. This is why threaded web servers like Puma work well for Ruby.
- CPU-bound work (heavy calculations in Ruby code) does not run faster with threads — they take turns. For that, use several processes, or Ractors (a later lesson).
Exceptions in threads
An exception inside a thread doesn't crash the main program
immediately; it's re-raised when you call value or join:
t = Thread.new { raise ArgumentError, "bad input" }
t.report_on_exception = false
begin
t.value
rescue ArgumentError => e
puts "Thread failed: #{e.message}"
end
Thread failed: bad input
By default Ruby also prints a warning when a thread dies from an
exception (report_on_exception), which is usually what you want.
Common mistakes
- Starting threads and never calling
join/value. When the main program ends, unfinished threads are killed. - Starting one thread per item for thousands of items. Use a fixed pool of workers — the next lesson shows how.
- Expecting threads to speed up number-crunching in CRuby.