Concurrency primitives in Ruby

Note: for parallelism, see Ractors

There’s 3 of them:

  • Thread
  • Mutex
  • Condition Variable

Thread

first  = Thread.new { puts 'Thread 1'; sleep 1 }
second = Thread.new { puts 'Thread 2'; sleep 1 }
third  = Thread.new { puts 'Thread 3'; sleep 1 }

puts 'Main Thread'

[first, second, third].map(&:join)

Takeaways:

  • Blocks execute concurrently, but not in parallel.
  • Pause a thread for a few seconds with #sleep.
  • Wait for threads to finish execution with the #join method.
  • Execution scheduling is non deterministic, output will have a different order over multiple runs.

Mutex

Syncronize access to a shared resource with a Mutex:

mutex = Mutex.new
bag   = []

first  = Thread.new { mutex.synchronize { bag << 1; bag << 2 } }
second = Thread.new { mutex.synchronize { bag << 4; bag << 5 } }
mutex.synchronize { bag << 7; bag << 8 }

[first, second].map(&:join)
puts bag.inspect

Notes:

  • This ensures that consecutive numbers are always going to be next eachother in the bag. Without the mutex they will be in random order.
  • Use the mutex everywhere you need access to the bag shared resource, even on the main thread.

Condition Variable

Pause a thread until it’s notified to start again.

mutex     = Mutex.new
condition = ConditionVariable.new
bag       = []

consumer_ready = false
producer_done  = false

consumer = Thread.new do
  mutex.synchronize {
    consumer_ready = true
    condition.wait(mutex) unless producer_done

    puts bag.inspect
  }
end

producer = Thread.new do
  mutex.synchronize {
    bag << 1
    bag << 2
    bag << 3

    condition.signal if consumer_ready
    producer_done = true
  }
end

[producer, consumer].map(&:join)

Details:

  • Makes the consumer wait until the producer has data ready to be processed.
  • Need to check if you can actually #wait or #signal.
  • No artificial sleeps needed, and no data races.
  • In the main thread, wait for the producer to finish first, before waiting on the consumer (using #join).