Skip to content
AgileLogics
7 min read

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.