The number-one, easiest way to make Ruby threads communicate and synchronize is to use the built-in Queue class. You can even see this in the Ruby docs: This class provides a way to synchronize communication between threads.
Unfortunately, a Queue isn’t always what we want. So, how can we build our own primitives that are still nice and thread-safe?
The answer is comprised of three pieces:
- Mutex: semaphore for coordinating access to shared data from multiple threads
- ConditionVariable: used to suspend execution of a thread while waiting for a signal
- Data: the data that needs to be protected
Let’s build a simple thread-safe mailbox. The basic pattern is: Always wrap your wait in a loop and the Mutex synchronize. We make sure to check our condition (in this case @data.nil?
) before waiting to avoid the case where the cv has already been signaled.
require 'thread'
class Mailbox
def initialize
@data = nil
@mutex = Mutex.new
@cv = ConditionVariable.new
end
def wait
@mutex.synchronize do
while @data.nil?
@cv.wait(@mutex)
end
data = @data
@data = nil
data
end
end
def signal(data)
@mutex.synchronize do
@data = data.clone
@cv.signal
end
end
end
Here’s a quick example:
require 'thread'
initial_data = {payload: [4,5]}
mailbox = Mailbox.new
# consumer
t1 = Thread.new do
d = mailbox.wait
puts "GOT d: #{d}"
end
# producer
t2 = Thread.new do
mailbox.signal "monkey pants #{i}"
end
t1.join
t2.join
puts "done"
Homework assignment for the reader: Use Mutex
and ConditionVariable
to implement your own ConcurrentQueue
. For more information, dig into the POSIX docs.