Ruby: Concurrency & Performance
The GIL (Global Interpreter Lock)
MRI Ruby (the standard implementation) has a Global VM Lock (GVL). Only one thread runs Ruby code at a time. I/O operations release the GVL, so threads still help with I/O-bound work. For CPU-bound parallelism: use Ractors (Ruby 3+), multiple processes, or JRuby/TruffleRuby.
Threads
# Threads share memory — good for I/O-bound concurrency
threads = (1..5).map do |i|
Thread.new do
response = Net::HTTP.get(URI("https://api.example.com/item/#{i}"))
JSON.parse(response)
end
end
results = threads.map(&:join).map(&:value) # wait and collect results
# Mutex — protect shared state
mutex = Mutex.new
counter = 0
threads = 10.times.map do
Thread.new do
1000.times do
mutex.synchronize { counter += 1 } # atomic increment
end
end
end
threads.each(&:join)
puts counter # 10000 — correct with mutex
# Thread-local variables
Thread.current[:request_id] = SecureRandom.uuid
Thread.current[:request_id] # only visible to this threadFibers
# Fibers — cooperative (not preemptive) concurrency, very lightweight
# Control passes explicitly via Fiber.yield and fiber.resume
producer = Fiber.new do
5.times do |i|
puts "Producing #{i}"
Fiber.yield i # pause, return value to caller
end
nil
end
loop do
value = producer.resume # resume from where it yielded
break if value.nil?
puts "Consumed #{value}"
end
# Enumerator uses Fibers internally
enum = Enumerator.new do |yielder|
yielder << 1
yielder << 2
yielder << 3
end
enum.next # 1
enum.next # 2
enum.next # 3Ractors (Ruby 3+)
# Ractors — true parallelism without GVL
# Ractors cannot share mutable objects (enforced at runtime)
# Communicate via message passing
r = Ractor.new do
msg = Ractor.receive # block until message arrives
msg.upcase
end
r.send("hello")
puts r.take # "HELLO"
# Parallel processing with a worker pool
workers = 4.times.map do
Ractor.new do
loop do
job = Ractor.receive
Ractor.yield job * 2
end
end
end
results = [1, 2, 3, 4].map.with_index do |n, i|
workers[i % workers.size].send(n)
workers[i % workers.size].take
endPerformance Tips
Frozen string literals: add # frozen_string_literal: true to every file — eliminates string allocation for all literals
Avoid N+1 queries: use includes(), eager_load(), or preload() in ActiveRecord. Use Bullet gem in development to detect N+1s automatically.
Use pluck() instead of map(&:attr) — returns raw values without instantiating models
Database indexes: index all foreign keys, frequently queried columns, and uniqueness constraints
Benchmark before optimizing: use the Benchmark module or benchmark-ips gem to measure before changing anything
Memory profiling: memory_profiler gem — find objects that are allocated but never freed
rack-mini-profiler: shows per-request SQL queries, timing, and memory in the browser
Sidekiq over Delayed::Job: Redis-backed, multi-threaded, much faster for background jobs
require 'benchmark/ips'
Benchmark.ips do |x|
x.report('string concat') { "Hello" + " " + "World" }
x.report('interpolation') { "Hello #{"World"}" }
x.report('frozen') { +"Hello".freeze }
x.compare!
end
# Check memory allocation
require 'memory_profiler'
report = MemoryProfiler.report do
1000.times { User.all.to_a }
end
report.pretty_printKeep your own version of these notes — editable, searchable, and organised by your stack.
Start free