Thread Safety: Mutex and Queue
Prerequisites: Threads and the GVL
The GVL stops two threads from running Ruby code at the same instant,
but it does not make your code thread-safe. A thread can be paused
in the middle of an operation like count += 1 (read, add, write), and
another thread can change count in between. The result: lost
updates, and bugs that appear only occasionally.
Mutex — one thread at a time
A Mutex (mutual exclusion lock) guards a piece of code so only one thread can run it at a time:
counts = Hash.new(0)
lock = Mutex.new
threads = 10.times.map do |i|
Thread.new do
1_000.times do
lock.synchronize { counts[i.even? ? :even : :odd] += 1 }
end
end
end
threads.each(&:join)
p counts
{even: 5000, odd: 5000}
Keep the code inside synchronize as short as possible — while one
thread holds the lock, every other thread that wants it waits.
Queue — hand work between threads safely
Queue is a thread-safe first-in, first-out list. It's the standard
way to build a worker pool: a fixed number of threads pulling jobs
until the queue is closed.
jobs = Queue.new
results = Queue.new
(1..9).each { |n| jobs << n }
jobs.close
workers = 3.times.map do
Thread.new do
while (n = jobs.pop) # pop returns nil once the queue is closed and empty
results << n * n
end
end
end
workers.each(&:join)
squares = []
squares << results.pop until results.empty?
p squares.sort
[1, 4, 9, 16, 25, 36, 49, 64, 81]
popwaits if the queue is empty, so idle workers sleep instead of spinning.closetells workers no more jobs are coming.- Results arrive in whatever order the workers finish, so sort them if order matters.
SizedQueue.new(100) adds a maximum size: producers wait when it's
full, which stops a fast producer from using unlimited memory.
Safer by design
The simplest thread-safe code shares nothing mutable: give each thread
its own data, and communicate results through a Queue or return
values. Freezing shared configuration (CONFIG = {...}.freeze) makes
accidental writes fail loudly.
Common mistakes
- Calling a method that takes the same lock while already holding it —
deadlock; recursive locking (ThreadError). Keep locked sections small and don't call out to unknown code inside them. - Assuming
ArrayandHashare thread-safe in general. They're safe enough under CRuby's GVL for simple single operations, but not on other Ruby implementations (JRuby, TruffleRuby) — and never for read-then-write sequences.