Note: for parallelism, see Ractors
There’s 3 of them:
- Thread
- Mutex
- Condition Variable
Thread Link to heading
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 Link to heading
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 thebag
shared resource, even on the main thread.
Condition Variable Link to heading
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 theproducer
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
).