Skip to content
AgileLogics
6 min read

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]
  • pop waits if the queue is empty, so idle workers sleep instead of spinning.
  • close tells 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 Array and Hash are 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.