Ruby Threads — Concurrent Programming with Thread Class, Mutex and Queue
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