Making our way through the Ruby Exception Handling series we have now arrived at the ThreadError
class. ThreadErrors
are raised anytime an invalid operation is attempted on a thread, plain and (somewhat) simple!
Throughout this article we'll examine the ThreadError
in a bit more detail, looking at where it sits in the Ruby Exception
class hierarchy and examining some simple example code that illustrates how ThreadErrors
are raised so you can (hopefully) avoid them in your own coding adventures. Let's get this party started!
The Technical Rundown
- All Ruby exceptions are descendants of the
Exception
class, or a subclass therein. StandardError
is a direct descendant of theException
class, and is also a superclass with many descendants of its own.ThreadError
is the direct descendant ofStandardError
.
When Should You Use It?
As previously mentioned a ThreadError
is the result of trying to perform an invalid operation on a thread in Ruby. Yet working with threads can be rather confusing so let's start with a brief overview of how we can use threading to handle simultaneous processing in our Ruby code.
The Thread
class provides a number of helper API methods, but since the purpose of using multiple threads is to perform concurrent actions we'll typically just start by instantiating a new thread with Thread::new
:
thread = Thread.new { puts 'My new thread is terminating.' }
Our new thread has a code block that tells it to execute a single statement of puts
for our string message. However, merely instantiating a new thread doesn't cause it to execute. As is normal, when executing any Ruby script we always start (and finish) with the main
thread. To get our new sub-thread to execute we need to join it to an active thread. This can be accomplished with the #join
method of the sub-thread we want to start. This suspends the current thread execution (our main
thread in this case) and starts execution of the sub-thread:
thread = Thread.new { puts 'My new thread is terminating.' }
thread.join #=> My new thread is terminating.
Since our code block for what the new thread should accomplish is so short we immediately get our message output and then the sub-thread is killed and the main thread resumes.
To expand on this a bit and see how ThreadErrors
are produced we'll take a look at some slightly more complex code that uses threading. Here we have a few helper functions and modules, but the main functions we care about are #get_threads
and #thread_example
:
module Logging
class << self
# Outputs the +message+ to console with timestamp.
# If +timestamp+ is +false+, only +message+ is output.
def log(message, timestamp=true)
puts "#{timestamp ? "[#{Time.now.strftime("%T")}] " : nil}#{message}"
end
end
enddef print_exception(exception, explicit)
puts "[#{explicit ? 'EXPLICIT' : 'INEXPLICIT'}] #{exception.class}: #{exception.message}"
puts exception.backtrace.join("\n")
enddef get_threads
threads = []
# Create first thread and add to array.
threads << Thread.new do
Logging.log 'Sub-thread 1 is sleeping.'
# Sleep for 2 seconds.
sleep(2)
Logging.log 'Sub-thread 1 is terminating.'
end
# Create second thread and add to array.
threads << Thread.new do
Logging.log 'Sub-thread 2 is terminating.'
end
# Return threads array.
threads
end
def thread_example
begin
Logging.log 'Main thread has started.'
# Loop through all threads.
get_threads.each do |thread|
# Join sub-threads to executing (main) thread.
thread.join
end
Logging.log 'Main thread is terminating.'
rescue ThreadError => e
print_exception(e, true)
rescue => e
print_exception(e, false)
end
en
#get_threads
creates a couple new sub-threads places them into an array that is returned. We also output a few log messages so we know when sub-thread 1
begins and ends its two-second sleep cycle. #thread_example
is triggered by the main
thread and takes our collection of sub-threads and #joins
them with the main
thread. The expected result is that we should see the main
thread start, then both our sub-threads terminate, and finally our main
thread should terminate at the end. Sure enough, running this code shows us that expected output:
[17:43:10] Main thread has started.
[17:43:10] Sub-thread 1 is sleeping.
[17:43:10] Sub-thread 2 is terminating.
[17:43:12] Sub-thread 1 is terminating.
[17:43:12] Main thread is terminating.
We didn't really try to do anything fancy with our threads in this example, so everything ran as expected, but what happens if we try to mix things up a bit and perform some actions on our threads that may be invalid or out of order? For example, the overall setup is the same where we can get a few sub-threads via #get_threads
, but this time let's try calling the Thread::stop
method while we're looping through our sub-threads to #join
them up:
def stop_thread_example
begin
Logging.log 'Main thread has started.'
# Loop through all threads.
get_threads.each do |thread|
# Join sub-threads to executing (main) thread.
thread.join
# Stop execution of the current (main) thread.
Thread.stop
end
Logging.log 'Main thread is terminating.'
rescue ThreadError => e
print_exception(e, true)
rescue => e
print_exception(e, false)
end
end
Suddenly we run into trouble and a ThreadError
is thrown, indicating that we've attempted to stop the only active thread (our main
thread):
[18:05:26] Main thread has started.
[18:05:26] Sub-thread 1 is sleeping.
[18:05:26] Sub-thread 2 is terminating.
[18:05:28] Sub-thread 1 is terminating.
[EXPLICIT] ThreadError: stopping only thread
note: use sleep to stop forever
Notice that we actually get all the output messages indicating our sub-threads have executed as expected before we get the ThreadError
. This is because of the ordering that we're executing everything in. The #join
call suspends the main thread temporarily and processes the sub-thread that is being called, so only after both of those terminate does the Thread::stop
call actually run into trouble, which is indicated by the error message -- we cannot stop a thread when it's the only active thread at the time.
To get the most out of your own applications and to fully manage any and all Ruby Exceptions, check out the Airbrake Ruby exception handling tool, offering real-time alerts and instantaneous insight into what went wrong with your Ruby code, including integrated support for a variety of popular Ruby gems and frameworks.