By default, Ruby programs run one task at a time. This means if one part of the code takes a long time to finish, the rest has to wait.
But what if we want to do multiple things at once—like downloading a file while showing a loading animation?
This is where multithreading comes in.
What is a Thread?
A thread is like a mini-program that runs inside our Ruby program. With multithreading, we can run multiple threads at the same time.
In Ruby, we can create threads using the Thread
class.
Creating Threads in Ruby
We use the Thread.new
method to create a new thread. Here's its syntax:
Thread.new do
# Code to run in a new thread
end
This syntax creates a child thread inside our main program.
But before we use this syntax to create an actual thread, let's first understand the difference between a main thread and child threads.
Main Thread vs Child Threads
When we run a Ruby program, it always starts with a main thread. Any threads we create using Thread.new
are child threads. For example,
# Create a new thread (child thread)
new_thread = Thread.new do
puts "Welcome to a new thread!"
puts "Exiting the thread..."
end
# Main thread starts here
# Wait for the child thread to finish
new_thread.join
# Print a message
puts "Welcome to the main program."
Output
Welcome to a new thread! Exiting the thread... Welcome to the main program.
Here, we created a child thread named new_thread
that prints a set of messages.
The join
method tells Ruby to wait for the thread to finish before moving on to the main thread.
Tip: Try removing the join
method from this program. You'll see that the execution moves on to the main program right away.
Concurrent Threads with Sleep
We can use the sleep
method to pause our thread for a specified amount of time. Let's use it to make the main thread and the child thread execute concurrently:
# Child thread
thread = Thread.new do
# Loop code 3 times
3.times do
puts "Child thread working..."
# Pause the thread for 1 second
sleep(1)
end
end
# Main thread
# Loop code 3 times
3.times do
puts "Main thread working..."
# Pause the thread for 1 second
sleep(1)
end
# Wait for the child thread to end
thread.join
Sample Output
Main thread working... Child thread working... Main thread working... Child thread working... Main thread working... Child thread working...
Here, the main and child threads each run a times
loop three times. In each iteration of the loops, the threads pause for one second.
1. Role of the 'join' Method
Notice that we have used the join
method only at the end of the program. So, the main thread starts its own loop without waiting for the child thread to finish.
2. Role of the 'sleep' Method
Thanks to the sleep
method in the loop of the main thread, the child thread will be executed in between the pauses.
Similarly, the sleep
method in the child thread ensures that the main thread gets executed in between the pauses.
3. Concurrent Threads
As a result, the main and child threads execute concurrently (interleaved). This means the outputs from the main and child threads appear mixed (not all from one, then the other).
Notes:
- Ruby does not guarantee exact interleaving or timing. Thread scheduling depends on the Ruby implementation and operating system.
- In standard Ruby (MRI), a Global Interpreter Lock (GIL) prevents true parallel execution of threads for CPU-bound tasks. However, threads are still useful for I/O-bound tasks, i.e., tasks concerning input/output.
- You can remove
thread.join
at the end of this program and it'll still work. However, there'll be no guarantee that the main thread will wait for the child thread to execute completely. Thus, it's safer to usejoin
.
Example 1: Basic Ruby Multithreading
thread = Thread.new do
puts "Thread starting..."
sleep(2)
puts "Thread done!"
end
puts "Waiting for thread..."
thread.join
puts "All done!"
Output
Waiting for thread... Thread starting... Thread done! All done!
Here's how this program works:
- The main thread first executes the
puts "Waiting for thread..."
. - It then encounters
thread.join
. As a result, it waits for the child thread (stored in thethread
variable) to complete. - In the child thread,
puts "Thread starting..."
is executed first. - The child thread then pauses for two seconds due to
sleep(2)
. - After the pause,
puts "Thread done!"
is executed. - Because the child thread has completed execution, the program goes back to the main thread.
- Finally, the main thread executes
puts "All done!"
Example 2: Running Multiple Threads
thread1 = Thread.new do
puts "Thread 1 starting"
sleep(1)
puts "Thread 1 finished"
end
thread2 = Thread.new do
puts "Thread 2 starting"
sleep(1)
puts "Thread 2 finished"
end
thread3 = Thread.new do
puts "Thread 3 starting"
sleep(1)
puts "Thread 3 finished"
end
# Wait for all threads to finish
thread1.join
thread2.join
thread3.join
puts "\nAll threads finished!"
Sample Output
Thread 1 starting Thread 2 starting Thread 3 starting Thread 1 finished Thread 2 finished Thread 3 finished All threads finished!
Here, we created three threads (thread1
, thread2
, and thread3
) and waited for them all to complete.
Notice that we created the threads separately, resulting in a long chain of codes:
thread1 = Thread.new do
# Code for thread1
end
thread2 = Thread.new do
# Code for thread2
end
thread3 = Thread.new do
# Code for thread3
end
We can rectify this issue by creating the threads inside a loop. Let's see how in the next example.
Example 3: Create Multiple Threads Using Loop and Array
Let's write a cleaner version of the previous example by creating the threads inside a loop and adding them to an array:
# Create an array to store multiple threads
threads = []
# Run a loop 3 times to create 3 separate threads
3.times do |i|
# Create a new thread and
# add it to the threads array
threads << Thread.new do
puts "Thread #{i + 1} starting"
sleep(1)
puts "Thread #{i + 1} finished"
end
end
# Iterate through the array
# Wait for each thread to execute
threads.each(&:join)
puts "\nAll threads finished!"
Sample Output
Thread 1 starting Thread 2 starting Thread 3 starting Thread 2 finished Thread 1 finished Thread 3 finished All threads finished!
1. Creating the Threads with Loop
First, we ran a times
loop three times to create three threads. In each iteration of the loop, a new thread is created and appended to the threads
array.
3.times do |i|
threads << Thread.new do
# Thread code
end
end
2. The Main Thread
In the main thread, we use an each
loop to iterate through the threads
array.
In each iteration of the loop, we use join
with the respective thread to ensure that the thread is executed completely:
threads.each(&:join)
The above code is a shorthand for the following:
threads.each { |thread| thread.join }
You can replace the each
loop shorthand in the example with the traditional each
loop code above.
Note: The output of this program may vary with each execution.
Shared Data and Race Conditions
All threads share the same memory. So, if multiple threads try to change the same variable at the same time, unexpected things can happen. For example,
# Create a counter variable
counter = 0
# Loop 3 times to create 3 threads
# And append them to the threads array
threads = 3.times.map do
Thread.new do
10.times do
# Increment counter
counter += 1
end
end
end
# Wait for the threads to execute
threads.each(&:join)
# Print the value of counter
puts "Counter: #{counter}"
Expected Output
Counter: 30
Possible Output 1
Counter: 26
Possible Output 2
Counter: 29
Here, we created the counter
variable in the main thread and initialized it to 0. Thus, this variable will be shared by all child threads.
1. Creating the Threads
Then, we created three child threads using the following code:
threads = 3.times.map do
Thread.new do
# Code
end
end
We use map
to create an array of threads by collecting the result of each Thread.new
call.
Each thread runs its own times
loop with 10 iterations. In each iteration of the loops, the value of the counter
variable is increased by 1.
Thread.new do
10.times do
# Increment counter
counter += 1
end
end
2. Ideal Scenario
Ideally, each thread executes without interference from the others, updating the counter
variable in the following way:
Thread | Loop Operation | counter (Final Value) |
---|---|---|
1 | Increment the value of counter 10 times. |
10 |
2 | Increment the value of counter 10 times. |
20 |
3 | Increment the value of counter 10 times. |
30 |
3. Practical Scenario
In reality, the multiple threads might try to simultaneously update the counter
variable.
For instance, if the first thread updates the value of counter
to 6, it's possible that another thread also updates its value to 6 at the same time.
So, instead of the first thread updating counter
to 6 and then the other thread updating it to 7, the update made by the first thread gets overwritten.
In this scenario, counter
will be updated to 6 and the final output will be less than 30 as a result.
Note: The scenario above is known as a race condition, when multiple threads attempt to read, modify, and write a shared resource without coordination.
In Ruby, we can prevent race conditions using a Mutex
.
Using Mutex to Prevent Race Conditions
To prevent race conditions, we use a Mutex
(mutual exclusion), which locks a section of code so only one thread can run it at a time.
You can think of a Mutex
like a lock on a bathroom door. Only one person (thread) can go inside at a time.
In general, we use Mutex
to control access to shared resources when multiple threads are running concurrently.
Syntax
mutex_object = Mutex.new
# Other code
mutex_object.synchronize do
# Code that accesses shared resources
end
Example 4: Ruby Mutex
Now, let's use a Mutex
to prevent race conditions from occurring in our previous program:
counter = 0
# Create a Mutex object
mutex = Mutex.new
threads = 3.times.map do
Thread.new do
10.times do
# Update counter inside the Mutex block
mutex.synchronize do
counter += 1
end
end
end
end
threads.each(&:join)
puts "Counter: #{counter}"
Output
Counter: 30
Here, we created a Mutex
object named mutex
. We then used this object to lock the thread when it's accessing the counter
variable (since it's a shared resource).
mutex.synchronize do
counter += 1
end
So, when a thread reaches this part of the code, it tries to lock on to the block of code inside mutex.synchronize do
:
- If
mutex
is already locked by another thread, the current thread waits until it is unlocked. - Once
mutex
is acquired (locked), the thread executes the block and incrementscounter
by 1. - After the block finishes,
mutex
is automatically unlocked, allowing other waiting threads to acquire it.
Since no thread is interfering with the execution of another, we always get the following output:
Counter: 30
Handling Errors in Threads
If an error happens inside a thread, it won't crash the main program but the thread will stop. For example,
# A thread that raises an error
Thread.new do
raise "Something went wrong"
end
# Pause main thread for 1 second
# So that the child thread can execute
sleep(1)
puts "Main program continues"
Output
#<Thread:0x00007c501ef63bd8 /tmp/ZXZLrHrmDX/main.rb:1 run> terminated with exception (report_on_exception is true): /tmp/ZXZLrHrmDX/main.rb:2:in 'block in <main>': Something went wrong (RuntimeError) Main program continues
Here, we created a thread that manually raises an error with the raise
keyword. This stops the execution of the thread.
You can handle such exceptions using begin...rescue
:
Thread.new do
# Put code that may cause error inside 'begin'
begin
raise "Error in thread"
# Rescue the error using object 'e'
rescue => e
puts "Caught error: #{e.message}"
end
end
sleep(1)
puts "Main program continues"
Output
ERROR! Caught error: Error in thread Main program continues
Here, we've kept the code that might cause an error inside the begin
statement. If an error occurs, the rescue
block handles the exception by printing an error message.
This way, even if a thread causes an error, its execution won't be stopped abruptly.
To learn more, visit Ruby Exception Handling.
More on Ruby Multithreading
1. When to Use Threads
Use threads when you want to:
- Perform multiple tasks at once.
- Avoid blocking the main program (e.g., during long-running tasks).
- Improve performance in I/O-heavy programs.
Note: Ruby uses a Global Interpreter Lock (GIL), which limits how much true parallelism is possible for CPU-bound tasks. But threads still help with tasks that wait on input/output (like file operations, network requests, or database access).
2. When Not to Use Threads
- For heavy CPU-bound tasks in Ruby (GIL limits performance).
- If you need true parallelism. In such cases, consider using processes or external libraries like
parallel
orconcurrent-ruby
.
- Always use
join
to wait for threads to complete. - Avoid shared variables unless using thread-safe methods.
- Use
Mutex
for safe access to shared data. - Handle exceptions inside threads.
- Keep thread logic simple and focused.
A process has its own memory, while threads share memory within the same process.
And while threads are lighter and faster to create, they don't offer true parallelism (unlike processes).
While threads share variables and resources that are declared in the main thread, they do not share variables that are declared inside them.
In other words, variables declared inside a thread are local to it. For example,
thread1 = Thread.new do
# Create a local variable 'num'
num = 5
puts num
end
# Invalid: Attempt to access 'num' in another thread
thread2 = Thread.new do
puts num
end
thread1.join
thread2.join
# Invalid: Attempt to access 'num' in main thread
puts num
Output
5 #<Thread:0x00007ddbefa53978 /tmp/XjZ6AVlftj/main.rb:8 run> terminated with exception (report_on_exception is true): /tmp/XjZ6AVlftj/main.rb:9:in 'block in <main>': undefined local variable or method 'num' for main (NameError)
Here, num
is a variable that's local to the scope of thread1
. As a result, we cannot access it from outside that thread.
You can attach the join
method to the end
statement of a thread. For example,
thread1 = Thread.new do
puts "First thread"
end.join
thread2 = Thread.new do
puts "Second thread"
end.join
puts "Main thread"
Output
First thread Second thread Main thread
As you can see, we've used end.join
to make the program wait for the threads to execute. This syntax is common in short thread blocks and helps reduce clutter.
The above program is equivalent to:
thread1 = Thread.new do
puts "First thread"
end
thread2 = Thread.new do
puts "Second thread"
end
thread1.join
thread2.join
puts "Main thread"
We can check the status of a thread using the alive?
method. For example,
thread = Thread.new do
sleep(2)
end
puts "Thread alive? #{thread.alive?}" # Output: true
sleep(3)
puts "Thread alive? #{thread.alive?}" # Output: false
Here, thread
pauses for two seconds before ending. So, it will be "alive" (running or sleeping) for two seconds.
As the child thread is sleeping, we check if it's alive in the main thread. Here's how the checking works:
1. The First Check
When we check the thread
for the first time, it's still running since two seconds haven't elapsed. Therefore, thread.alive?
returns true
.
2. The Second Check
After the first check, the main thread sleeps for three seconds. During this time, the child thread has already finished its execution.
Thus, the second thread.alive?
returns false
.
We can use the value
method to get the last evaluated expression of a thread. For example,
thread = Thread.new do
5 * 2
end
puts thread.value
# Output: 10
Here, thread.value
gives us the result of the thread operation 5 * 2
, which is 10.
Note: With thread.value
, the thread blocks until the result is available. This is similar to calling join
and then retrieving the result.