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.