Skip to Content
Course content

206: Parallel Processing with the parallel Gem

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Whenever I'm dealing with I/O-bound tasks—like hitting an API a hundred times or resizing a folder full of images—the standard Ruby .each loop starts to feel like a bottleneck. We're spending most of our time waiting for the network or the disk, while our CPU just sits there idling. That's where the parallel gem comes in. It's a wrapper that makes it trivial to swap out a sequential loop for one that leverages multiple cores or threads.

The slow way to fetch profiles

Let's imagine we're building a tool to sync user data from a legacy API. I've got a list of user IDs, and I need to fetch a profile for each one. Since this is a "legacy" API, it's slow—maybe it takes a full second per request.

# First, gem install parallel
require 'parallel'

user_ids = (1..10).to_a

# Simulating a slow API call
def fetch_profile(id)
  sleep(1) 
  { id: id, name: "User #{id}", status: "Active" }
end

start_time = Time.now
user_ids.each do |id|
  puts "Fetching #{id}..."
  fetch_profile(id)
end
puts "Finished in #{Time.now - start_time} seconds"
# This takes exactly 10 seconds. Painful.

Flipping the switch to parallel

The beauty of the parallel gem is that the API mirrors Ruby's Enumerable methods. I can basically just replace .each with Parallel.each. By default, this gem uses processes (forking), which is great because it bypasses Ruby's Global Interpreter Lock (GIL), allowing us to actually use multiple CPU cores.

start_time = Time.now
Parallel.each(user_ids, in_processes: 4) do |id|
  puts "Fetching #{id} in process #{Process.pid}..."
  fetch_profile(id)
end
puts "Finished in #{Time.now - start_time} seconds"
# Now this takes about 3 seconds. Much better.

The "where did my data go?" moment

Here is where I usually trip up when I first start using this gem. I remember trying to collect the results of these API calls into a local array, like this:

results = []
Parallel.each(user_ids, in_processes: 4) do |id|
  results << fetch_profile(id)
end

puts "Total results: #{results.length}" 
# Expected: 10. Actual: 0. 

I stared at this for five minutes before remembering how forking works. When Parallel uses in_processes, it creates a copy of the entire Ruby process. Each child process has its own version of the results array. They push the data into their own local copy, and when the child process dies, that memory vanishes. The original results array in the parent process stays completely empty.

Using map for a clean result set

If you actually need the data back from your parallel loop, don't try to push to an external array. Instead, use Parallel.map. This tells the gem to collect the return value of each block and aggregate them back into a single array in the parent process once everything is finished.

start_time = Time.now

# map collects the return values from the child processes
results = Parallel.map(user_ids, in_processes: 4) do |id|
  fetch_profile(id)
end

puts "Collected #{results.length} profiles."
puts "Finished in #{Time.now - start_time} seconds"

One last tip: if your task is purely I/O (like waiting for a website to respond) and doesn't require heavy CPU lifting, you can use in_threads: 4 instead of in_processes. Threads are "lighter" on system resources than processes, though you have to be more careful about thread-safety if you're mutating shared objects.




📋 Practical Task

Building a Parallel Website Status Checker

You need to create a script that checks the HTTP status of a list of websites. Instead of checking them one by one, you'll use the parallel gem to speed up the process.

  • Create an array of at least 5 URLs (e.g., 'https://www.google.com', 'https://www.github.com', etc.).
  • Use net/http and uri to write a method that takes a URL and returns a string: "URL: [url] - Status: [code]".
  • Use Parallel.map with in_threads: 5 to process the URLs.
  • Print the final array of results to the console.
  • Ensure you wrap the execution in a timer (using Time.now) to prove that the parallel execution is faster than a standard .map loop.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.