Reason #130 • May 10th, 2026

Parallelizing compute with the parallel gem

As we discussed in the posts related to Thread.new for concurrency, Ruby does not provide parallelism out of the box with threads alone. Due to the Global Interpreter Lock, a single Ruby process using threads for concurrency will only ever saturate a single CPU core.

Therefore, the go-to solution for parallelism in Ruby is to use multiple processes. In the post on forking, we could see how a Ruby web server might use fork to create multiple worker processes to handle incoming requests in parallel.

However, since manually managing inter-process communication can be tedious, the parallel gem can be a great help. It provides a simple interface for running code in parallel across multiple processes:

Ruby
require "benchmark"
require "parallel"

WIDTH = 1_200
HEIGHT = 800
MAX_ITERATIONS = 1_000

def mandelbrot_iterations(x, y)
  real = -2.0 + x * 3.0 / WIDTH
  imaginary = -1.2 + y * 2.4 / HEIGHT
  zr = 0.0
  zi = 0.0
  iterations = 0

  while iterations < MAX_ITERATIONS && zr * zr + zi * zi <= 4.0
    zr, zi = zr * zr - zi * zi + real, 2.0 * zr * zi + imaginary
    iterations += 1
  end

  iterations
end

Benchmark.bm do |bm|
  bm.report("non-parallel") do
    row_totals = (0...HEIGHT).map do |y|
      (0...WIDTH).sum { |x| mandelbrot_iterations(x, y) }
    end

    puts "Mandelbrot escape iterations: #{row_totals.sum}"
    # => Mandelbrot escape iterations: 206825643
  end

  bm.report("parallel") do
    row_totals = Parallel.map(0...HEIGHT) do |y|
      (0...WIDTH).sum { |x| mandelbrot_iterations(x, y) }
    end

    puts "Mandelbrot escape iterations: #{row_totals.sum}"
    # => Mandelbrot escape iterations: 206825643
  end
end

# Output will vary by machine. Here's what I got on my Apple M1 Pro:
#                   user     system      total        real
# non-parallel 22.118777   0.090735  22.209512 ( 22.262529)
# parallel      0.017934   0.029973  25.630064 (  3.002575)
    

Hardly anything changed in the code, we just replaced map with Parallel.map. But we calculated the results more than 7x faster!

Note that, by default, Parallel.map will use as many processes as there are CPU cores. You can customize this with the in_processes option, e.g. Parallel.map(..., in_processes: 4).

If you're running a recent version of Ruby, you may also want to try out the in_ractors option, which uses Ractors instead of processes for parallelism. But that's a topic for another day.

History

Michael Grosser first published the parallel gem to RubyGems in 2009. Its enumerable-like API and fork-based implementation have remained mostly unchanged since then.

The newer in_ractors option follows Ruby's introduction of Ractor in Ruby 3.0, released in December 2020. The parallel gem added Ractor support in version 1.22.0, released in March 2022.

Reason #131 ?