Backend Development 11 min read

Understanding Ruby Multithreading and Multiprocessing

This article explains the differences between Ruby threads and processes, when to use each for performance gains, illustrates practical scenarios, and provides code examples for simple multithreading, multiprocessing, and using the Parallel gem.

Liulishuo Tech Team
Liulishuo Tech Team
Liulishuo Tech Team
Understanding Ruby Multithreading and Multiprocessing

The article introduces Ruby's multithreading and multiprocessing concepts, motivated by interview observations and real‑world business needs, aiming to help readers grasp when and how to use these concurrency models.

Objectives

Understand the difference between threads and processes in Ruby.

Identify situations where multithreading or multiprocessing can improve performance.

Prerequisite Knowledge about Ruby Threads

Threads share the program's memory, using fewer resources.

Threads are lighter weight than processes and start faster.

Inter‑thread communication is simple.

Because of Ruby's Global Interpreter Lock (GIL), multiple threads cannot run on multiple CPUs simultaneously.

Prerequisite Knowledge about Ruby Processes

Processes cannot share memory for read/write.

Since Ruby 2.0, Copy‑On‑Write allows forked processes to share memory until a write occurs.

Each process can run on a different CPU core, better utilizing multi‑core CPUs.

Process isolation improves safety by avoiding data races common in multithreading.

Inter‑process communication is comparatively more difficult.

Application Scenarios for Threads and Processes

Scenario 1

A person (Mr. A) must collect personal information from 20 employees in the same room by handing out paper forms, waiting for each to be filled, and then collecting them.

Three approaches are analyzed:

Single‑process, single‑thread: Mr. A sends a form, waits for it to be returned, then proceeds to the next employee – very inefficient.

Multi‑process style: Mr. A hires four assistants, each handling five distinct employees; this leverages more resources and speeds up completion.

Multi‑thread style: Mr. A sends all forms without waiting, then later gathers the completed ones; computation proceeds while I/O is pending.

Scenario 2

The same task but employees are located several kilometers apart, turning the travel time into a CPU‑like cost.

The three approaches are revisited, showing that multi‑process distribution dramatically reduces total travel time, while multithreading offers little benefit because the “CPU” work (travel) dominates.

Scenario Summary

In I/O‑bound situations, both multithreading and multiprocessing can improve efficiency to varying degrees.

In CPU‑bound situations, multithreading provides negligible performance gains due to the GIL.

Combining processes and threads can be advantageous in mixed workloads.

Practical considerations such as budget (available CPU cores), limited I/O bandwidth, and management overhead also influence the choice of concurrency model.

Code Examples

Simple Ruby Multithreading

a = [1, 2, 3, 4]
b = []
mutex = Mutex.new

a.length.times.map do |i|
  Thread.new do
    v = [i, i ** 2].join(' - ')
    mutex.synchronize { b << v }
  end
end.map(&:join)

puts b
# => 2 - 4
#    1 - 1
#    0 - 0
#    3 - 9

When using threads, protect shared resources with a mutex and avoid placing long‑running operations inside the critical section.

Simple Ruby Multiprocessing

require 'socket'

MAX_RECV = 100

sockets = 3.times.map do |i|
  parent_socket, child_socket = Socket.pair(:UNIX, :DGRAM, 0)
  fork do
    pid = Process.pid
    parent_socket.close
    number = child_socket.recv(MAX_RECV).to_i
    puts "#{Time.now} process #{pid}# receive #{number}"
    sleep 1
    child_socket.write("#{number} - #{number * 2}")
    child_socket.close
  end
  child_socket.close
  parent_socket
end

puts "send jobs"
sockets.each_with_index.each do |socket, index|
  socket.send((index + 1).to_s, 0)
end

puts "read result"
sockets.map do |socket|
  puts socket.recv(MAX_RECV)
  socket.close
end

# => send jobs
#    read result
#    2016-04-03 11:30:34 +0800 process 21723# receive 1
#    2016-04-03 11:30:34 +0800 process 21724# receive 2
#    2016-04-03 11:30:34 +0800 process 21725# receive 3
#    1 - 2
#    2 - 4
#    3 - 6

Processes cannot share memory directly, so communication is performed via Unix sockets rather than appending to a shared array.

Using the Parallel Gem for Simpler Concurrency

require 'parallel'

list = 10.times.to_a
proc = Proc.new { list.pop || Parallel::Stop }
result = Parallel.map(proc, in_threads: 3) do |number|
  sleep 0.5
  puts "process #{Process.pid} receive #{number}\n"
  number.to_i * 2
end

puts "result: #{result.join('-')}"
# => process 21738 receive 9
#    process 21738 receive 7
#    ...
#    result: 18-16-14-12-10-8-6-4-2-0

Refer to the Parallel gem documentation for more advanced usage.

Conclusion

The concepts covered are basic but essential; understanding when to apply threads, processes, or a combination helps avoid misusing concurrency and improves overall system performance.

ConcurrencyMultithreadingGILRubyMultiprocessingParallel
Liulishuo Tech Team
Written by

Liulishuo Tech Team

Help everyone become a global citizen!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.