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.
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 - 9When 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 - 6Processes 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-0Refer 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.
Liulishuo Tech Team
Help everyone become a global citizen!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.