Skip to content

Ruby Threads — Concurrent Programming with Thread Class, Mutex and Queue

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Ruby Threads. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby threads enable concurrent execution using Thread class with mutex for synchronization, Queue for safe data sharing, and ThreadGroup for lifecycle management.

What You'll Learn

  • Creating and managing threads
  • Thread synchronization with Mutex
  • Thread-safe data structures (Queue, SizedQueue)
  • Thread lifecycle and variables

Why It Matters

Ruby threads handle I/O-bound tasks efficiently. Web servers (Puma, Unicorn) use threads to handle concurrent requests. DodaZIP uses threads for parallel HTTP downloads and file processing.

Real-World Use

Web request handling, background job processing, parallel API calls, file processing pipelines, and real-time event streaming.

flowchart LR
    A["Threads"] --> B["Thread Class"]
    B --> C["Synchronization"]
    C --> D["Queue/SizedQueue"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Creating Threads

thread = Thread.new do
  puts "Running in thread"
  sleep 1
  "result"
end

puts "Running in main"
value = thread.value
puts "Thread returned: #{value}"
# Running in main
# Running in thread
# Thread returned: result

Thread Synchronization with Mutex

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 "Counter: #{counter}"
# Counter: 10000

Thread-Safe Queue

queue = Queue.new
worker = Thread.new do
  while item = queue.pop
    puts "Processing: #{item}"
  end
end

queue << "task 1"
queue << "task 2"
queue << "task 3"
queue.close

worker.join
# Processing: task 1
# Processing: task 2
# Processing: task 3

Thread Lifecycle

thread = Thread.new do
  sleep 1
end

puts "Status: #{thread.status}"
thread.join
puts "Alive: #{thread.alive?}"
puts "Status after: #{thread.status}"
# Status: run
# Alive: false
# Status after: false

Thread Variables

thread = Thread.new do
  Thread.current[:user] = "alice"
  sleep 0.1
  puts "User: #{Thread.current[:user]}"
end

thread.join
# User: alice

Thread Group

group = ThreadGroup.new
threads = 3.times.map do |i|
  Thread.new(i) do |n|
    puts "Thread #{n}"
  end
end

threads.each { |t| group.add(t) }
group.list.each { |t| t.join }

Common Mistakes

1. Race Condition Without Mutex

counter = 0
10.times.map { Thread.new { 1000.times { counter += 1 } } }.each(&:join)
puts counter  # May not be 10000

2. Deadlock with Nested Locks

mutex1 = Mutex.new
mutex2 = Mutex.new

Thread.new { mutex1.synchronize { sleep 1; mutex2.synchronize {} } }
Thread.new { mutex2.synchronize { sleep 1; mutex1.synchronize {} } }

3. Exception Handling

Thread.new { raise "error" }  # Silently dies
Thread.abort_on_exception = true  # Or handle exceptions

4. Modifying Shared State

array = []
10.times.map { Thread.new { 100.times { array << 1 } } }.each(&:join)
puts array.size  # May not be 1000

5. Thread Starvation

Creating too many threads can degrade performance. Use thread pools or Ractors.

Practice Questions

1. What is a race condition? Two threads modify shared data without synchronization, causing unpredictable results. Fix with Mutex.

2. When does Queue#pop block? When the queue is empty. Blocks until an item is available or the queue is closed.

3. What is Thread#value? Blocks until the thread finishes and returns its last expression. Like join + return value.

4. How do you handle thread exceptions? Set Thread.abort_on_exception = true to exit on exception, or rescue inside the thread.

Challenge: Build a parallel web scraper using Queue to Process 20 URLs across 5 worker threads.

Solution
require 'net/http'
require 'uri'

urls = 20.times.map { "https://example.com" }
queue = Queue.new
urls.each { |u| queue << u }

workers = 5.times.map do
  Thread.new do
    while url = queue.pop(true) rescue nil
      uri = URI(url)
      response = Net::HTTP.get_response(uri)
      puts "#{url}: #{response.code}"
    end
  end
end

workers.each(&:join)

FAQ

{{< faq question="Are Ruby threads truly parallel?" >}} No, due to the GIL (Global Interpreter Lock). MRI Ruby threads are concurrent but not parallel for CPU-bound tasks. JRuby and TruffleRuby provide true parallelism. Ractors (Ruby 3) provide parallelism without GIL. {{< /faq >}}

{{< faq question="When should I use threads vs Ractors?" >}} Threads for I/O-bound work. Ractors for CPU-bound parallel processing. Ractors are safer because they don't share state. {{< /faq >}}

{{< faq question="What is SizedQueue?" >}} A Queue with a maximum size. push blocks when full, pop blocks when empty. Useful for rate-limiting producer threads. {{< /faq >}}

{{< faq question="Can threads share variables?" >}} Yes, but you must synchronize access with Mutex. Thread-local variables with Thread.current[:key] are safe. {{< /faq >}}

{{< faq question="What happens when main thread exits?" >}} All threads are killed. Use join to wait for threads to finish before the program exits. {{< /faq >}}

Try It Yourself

queue = Queue.new
producer = Thread.new do
  5.times { |i| queue << i; sleep 0.1 }
  queue.close
end

consumer = Thread.new do
  while item = queue.pop
    puts "Got: #{item}"
  end
end

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

Expected output:

Got: 0
Got: 1
Got: 2
Got: 3
Got: 4

What's Next

Now that you understand threads, explore Fibers for lightweight concurrency and Ractors for parallel execution.

Topic Description Link
Ruby Fibers Lightweight concurrency {{< ref "41-fibers" >}}
Ruby Ractors Parallel execution {{< ref "42-ractors" >}}
Go Goroutines Compare with Go's approach Go

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro