Lifecycle of a Thread
I’ve given you a few little tastes of the Thread
API already at this point. Now I’ll intentionally show you a bit more of the API that defines the lifecycle of a Thread.
require ‘thread’
puts defined?(Thread) #=> constant
puts defined?(Queue) #=> nil
require 'thread'
Funnily enough, require 'thread'
doesn’t load the Thread
constant. Thread
is loaded by default, but requiring 'thread'
brings in some utility classes like Queue
.
Thread.new
You spawn a thread by passing a block to Thread.new
(or one of its aliases), and optionally passing in block variables like in the Thread.start
example.
Thread.new { ... }
Thread.fork { ... }
Thread.start(1, 2) { |x, y| x + y }
This reveals a few interesting properties.
Executes the block. You pass a block when spawning the thread. The thread will yield to that block. Either it will reach the end of the block, or an exception will be raised. In either case, the thread terminates.
Returns an instance of Thread
. Like all constructors, Thread.new
returns a new instance. Realize that Thread.new
returns a Thread
instance representing the sub-thread that was just spawned. Like any other object, calling methods on the new Thread
instance will affect the spawned thread, not the current thread.
This is the starting point for any thread that you’re going to create.
Thread#join
Once you’ve spawned a thread, you can use #join
to wait for it to finish.
thread = Thread.new { sleep 3 }
thread.join
puts "You'll have to wait 3 seconds to see this"
If you run the above code example without calling #join
, you wouldn’t have to wait for 3 seconds. Without #join
, the main thread would exit before the sub-thread can execute its block. Using #join
provides a guarantee in this situation.
Calling #join
on the spawned thread will join the current thread of execution with the spawned one. In other words, where there were previously two independent threads of execution, now the current thread will sleep until the spawned thread exits.
Thread#join and exceptions
When joining two threads, you have to recall how a thread can terminate. In one case, a thread finishes executing its block and then terminates. This is the happy path.
In the other case, a thread raises an unhandled exception before it finishes executing its block. When one thread raises an unhandled exception, it terminates the thread where the exception was raised, but doesn’t affect other threads.
Similarly, a thread that crashes from an unhandled exception won’t be noticed until another thread attempts to join it.
thread = Thread.new do
raise 'hell'
end
# simulate work, the exception is unnoticed at this point
sleep 3
# this will re-raise the exception in the current thread
thread.join
This shows the literal meaning of ‘join.’ When one thread has crashed with an unhandled exception, and another thread attempts to join it, the exception is re-raised in the joining thread.
Here’s the output of this example in MRI:
code/snippets/exception_on_join.rb:2:in `block in <main>': hell (RuntimeError)
If you take a close look at the backtrace, you can see that it properly places the site of the exception on line 2, rather than on the line where the join occured.
Thread#value
Thread#value
is very similar to #join
: it first joins with the thread, and then returns the last expression from the block of code the thread executed.
thread = Thread.new do
400 + 5
end
puts thread.value #=> 405
The #value
method has the same properties as #join
regarding unhandled exceptions because it actually calls #join
. The only difference is in the return value.
Thread#status
Every thread has a status, accessible from Thread#status
.
It’s probably most common for one thread to check the status of some other thread, but it is possible for a thread to check its own status using Thread.current.status
.
Ruby defines several possible values for Thread#status
.
'run'
: Threads currently running have this status.'sleep'
: Threads currently sleeping, blocked waiting for a mutex, or waiting on IO, have this status.false
: Threads that finished executing their block of code, or were successfully killed, have this status.nil
: Threads that raised an unhandled exception have this status.aborting
: Threads that are currently running, yet dying, have this status.
adder = Thread.new do
# Here this thread checks its own status.
Thread.current.status #=> 'run'
2 * 3
end
puts adder.status #=> 'run'
adder.join
puts adder.status #=> false
Thread.stop
This method puts the current thread to sleep and tells the thread scheduler to schedule some other thread. It will remain in this sleeping state until its alternate, Thread#wakeup
is invoked. Once #wakeup
is called, the thread is back into the thread scheduler’s realm of responsibility.
thread = Thread.new do
Thread.stop
puts 'Hello there'
end
# wait for the thread to trigger its stop
nil until thread.status == 'sleep'
thread.wakeup
thread.join
Thread.pass
This one is similar to Thread.stop
, but instead of putting the current thread to sleep, it just asks the thread scheduler to schedule some other thread. Since the current thread doesn’t sleep, it can’t guarantee that the thread scheduler will take the hint.
Avoid Thread#raise
This method should not be used. It doesn’t properly respect ensure
blocks, which can lead to nasty problems in your code.
In terms of functionality, this method will allow a caller external to the thread to raise an exception inside the thread. In fact, the backtrace will actually point to whatever line the thread happened to be executing when this is called, which is not useful for debugging.
$cleaned_up = false
t = Thread.new do
begin
# nada
ensure
sleep
$cleaned_up = true
end
end
nil until t.status == 'sleep'
# At this point, the thread should be sleeping
# inside the ensure block. Now raise an exception
# inside the thread.
t.raise 'hell'
# Joining with the thread will cause the exception
# to be re-raised here.
begin
t.join
rescue
end
# This value is false because the ensure block
# was aborted when Thread#raise was called. This
# breaks the contract that ensure blocks provide.
puts $cleaned_up #=> false
Adapted from an example by James Tucker.
Avoid Thread#kill
This method should not be used for the exact same reason as the one above.
Supported across implementations
This Thread
API is a Ruby API. I’ve hinted that the different Ruby implementations have different underlying threading behaviours. That’s certainly the case, but all the Ruby implementations we’re looking at in this book support this same Thread
API.