ZetCode

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.

basic_threads.rb
#!/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.

args_values.rb
#!/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.

mutex_counter.rb
#!/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.

queue_producer.rb
#!/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.

thread_local.rb
#!/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.

current_thread.rb
#!/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.