Lesson 18 / 25

Threads, the GVL, Fibers and Ractors

Choose the right concurrency tool in CRuby.

Concurrency in CRuby

CRuby has a Global VM Lock (GVL): only one thread executes Ruby code at a time within a process. Threads still help with I/O-bound work, because the GVL is released while a thread waits on the network, disk or database, so several HTTP calls can run concurrently. They do not speed up CPU-bound Ruby code. For CPU parallelism, use multiple processes (as web servers such as Puma in clustered mode do, with several worker processes each running threads) or Ractors, an experimental actor-like model (Ruby 3.0+) where each Ractor runs in parallel with isolated objects and communicates by message passing; many gems are not yet Ractor-safe. Fibers are lightweight cooperative coroutines; with the Fiber scheduler interface (Ruby 3.0+) and libraries such as the async gem, thousands of concurrent I/O operations can run efficiently on one thread. Protect shared mutable state between threads with a Mutex (or use thread-safe structures such as Queue), and remember that background job systems such as Sidekiq handle much real-world concurrency for Rails apps.

Concurrent HTTP calls with threads and a thread-safe queue

Threads help while waiting on I/O; a Mutex protects shared state.

require "net/http"
require "json"

urls = %w[
  https://api.example.com/products/1
  https://api.example.com/products/2
  https://api.example.com/products/3
].map { URI(_1) }

results = {}
lock = Mutex.new

threads = urls.map do |uri|
  Thread.new do
    body = Net::HTTP.get(uri)                 # GVL released while waiting on the network
    lock.synchronize { results[uri.path] = JSON.parse(body) }
  rescue StandardError => e
    lock.synchronize { results[uri.path] = { error: e.message } }
  end
end
threads.each(&:join)
puts results.keys.inspect

jobs = Queue.new                               # thread-safe work queue
%w[pen ink notebook].each { jobs << _1 }
jobs.close
workers = 2.times.map { Thread.new { while (sku = jobs.pop) do puts "processing #{sku}" end } }
workers.each(&:join)

One microphone at a meeting

The GVL is a single microphone: only one person (thread) can speak Ruby at a time. But while someone waits for a phone call (I/O), they pass the microphone on, so a meeting full of people waiting on calls still moves quickly.

Quick check: In CRuby, when do threads improve performance?

  • For I/O-bound work such as network or database calls, because the GVL is released while waiting
  • For CPU-heavy calculations in pure Ruby
  • Never
  • Only on Windows
Answer

For I/O-bound work such as network or database calls, because the GVL is released while waiting — The GVL limits Ruby code to one thread at a time, but waiting on I/O releases it.