Worker Pools in Ruby

Our example demonstrates how to implement a worker pool using threads and queues in Ruby.

require 'thread'

# Here's the worker, of which we'll run several
# concurrent instances. These workers will receive
# work on the `jobs` queue and send the corresponding
# results on `results`. We'll sleep a second per job to
# simulate an expensive task.
def worker(id, jobs, results)
  while (job = jobs.pop)
    puts "worker #{id} started  job #{job}"
    sleep 1
    puts "worker #{id} finished job #{job}"
    results << job * 2
  end
end

# In the main function, we'll create our pool of workers
def main
  num_jobs = 5
  jobs = Queue.new
  results = Queue.new

  # This starts up 3 workers, initially blocked
  # because there are no jobs yet.
  workers = (1..3).map do |id|
    Thread.new { worker(id, jobs, results) }
  end

  # Here we send 5 `jobs` and then `close` that
  # queue to indicate that's all the work we have.
  num_jobs.times { |j| jobs << j + 1 }
  jobs.close

  # Finally we collect all the results of the work.
  # This also ensures that the worker threads have
  # finished.
  num_jobs.times { results.pop }

  # Wait for all worker threads to finish
  workers.each(&:join)
end

main

Our running program shows the 5 jobs being executed by various workers. The program only takes about 2 seconds despite doing about 5 seconds of total work because there are 3 workers operating concurrently.

$ ruby worker_pools.rb
worker 1 started  job 1
worker 2 started  job 2
worker 3 started  job 3
worker 1 finished job 1
worker 1 started  job 4
worker 2 finished job 2
worker 2 started  job 5
worker 3 finished job 3
worker 1 finished job 4
worker 2 finished job 5

real    0m2.015s
user    0m0.015s
sys     0m0.000s

This example demonstrates how to use Ruby’s Thread class and Queue class to implement a worker pool pattern. The Queue class is thread-safe, making it suitable for passing jobs between threads. The Thread.new method is used to create new threads for the workers.

Note that Ruby’s Global Interpreter Lock (GIL) can impact the performance of multi-threaded Ruby programs, especially for CPU-bound tasks. For I/O-bound tasks or when using JRuby or TruffleRuby (which don’t have a GIL), you may see more significant performance improvements with this pattern.