Ruby Threads
last modified October 18, 2026
Threads let a Ruby program run several units of work concurrently. This tutorial covers creating threads, passing arguments, retrieving return values, protecting shared state with a mutex, coordinating work with a queue, and thread-local storage. All examples produce deterministic output: results are collected and sorted or joined in a fixed order.
Thread Creation and Join
Thread.new starts a block in a new thread and returns a Thread object. The join method blocks the calling thread until that thread finishes. Values are read in a fixed order after joining, so the output does not depend on scheduling.
#!/usr/bin/ruby
t1 = Thread.new { "Hello from thread 1" }
t2 = Thread.new { "Hello from thread 2" }
t1.join
t2.join
puts t1.value
puts t2.value
puts "Main thread done"
The two threads each return a string. Calling join on each thread in turn makes the main thread wait for both before printing. The final line is printed only after all joins complete.
$ ./basic_threads.rb Hello from thread 1 Hello from thread 2 Main thread done
Passing Arguments and Return Values
Arguments given to Thread.new are passed to the block as parameters. The value method joins the thread and returns the value of the block's last expression.
#!/usr/bin/ruby
def square(n)
n * n
end
threads = (1..5).map do |i|
Thread.new(i) do |n|
square(n)
end
end
results = threads.map(&:value)
puts "Results: #{results.inspect}"
puts "Sum: #{results.sum}"
Each thread receives its number as an argument and returns its square. Collecting the values with map(&:value) keeps them in the order the threads were created, so the printed list is deterministic.
$ ./args_values.rb Results: [1, 4, 9, 16, 25] Sum: 55
Protecting Shared State with Mutex
Several threads increment a shared counter. Mutex#synchronize allows only one thread at a time to run its block, so the final total is exact.
#!/usr/bin/ruby
counter = 0
mutex = Mutex.new
threads = 10.times.map do
Thread.new do
1000.times do
mutex.synchronize { counter += 1 }
end
end
end
threads.each(&:join)
puts "Final counter: #{counter}"
Ten threads each increment the counter 1000 times inside synchronize. Because each increment is protected, the total is always 10000. Without the mutex, concurrent updates could be lost and the total would be unpredictable.
$ ./mutex_counter.rb Final counter: 10000
Producer and Consumer with Queue
A Queue is a thread-safe FIFO structure. One producer thread pushes a fixed number of items, followed by one sentinel per worker to signal shutdown.
#!/usr/bin/ruby
queue = Queue.new
results = Queue.new
ITEMS = 10
WORKERS = 3
producer = Thread.new do
ITEMS.times { |i| queue << i + 1 }
WORKERS.times { queue << :done }
end
workers = WORKERS.times.map do
Thread.new do
loop do
item = queue.pop
break if item == :done
results << item * item
end
end
end
producer.join
workers.each(&:join)
squares = []
squares << results.pop until results.empty?
puts "Processed #{squares.size} items"
puts "Sorted squares: #{squares.sort.inspect}"
Three workers pop items from the queue until they receive the sentinel. Their results go into a second queue. Because workers finish in no fixed order, the squares are sorted before printing.
$ ./queue_producer.rb Processed 10 items Sorted squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Thread-Local Storage
Thread#[] and Thread#[]= store values that are local to a thread, keyed by symbol. Each thread sets its own value, and the threads do not affect one another.
#!/usr/bin/ruby
threads = %w[alpha beta gamma].map do |name|
Thread.new(name) do |n|
Thread.current[:name] = n
Thread.current[:name].upcase
end
end
threads.each do |t|
puts "Thread-local value: #{t.value}"
end
puts "Main thread local: #{Thread.current[:name].inspect}"
Each thread stores its name under the :name key and returns its uppercase form. The threads are joined in creation order. The main thread's own :name value was never set, so it reads nil.
$ ./thread_local.rb Thread-local value: ALPHA Thread-local value: BETA Thread-local value: GAMMA Main thread local: nil
Thread.current and Thread.main
Thread.current returns the thread that is running the code, and Thread.main returns the main thread. The alive? method reports whether a thread is still running.
#!/usr/bin/ruby
main = Thread.current
puts "Main thread is main: #{Thread.main == main}"
t = Thread.new { Thread.current }
child = t.value
puts "Child is not main: #{child != main}"
puts "Child alive before join: #{child.alive?}"
t.join
puts "Child alive after join: #{child.alive?}"
Inside the main program, Thread.current equals Thread.main. A child thread returns its own Thread object. After join, the child is no longer alive.
$ ./current_thread.rb Main thread is main: true Child is not main: true Child alive before join: false Child alive after join: false
List all Ruby tutorials.
Author
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.