在Ruby线程中处理异常抛出

37

我正在寻找经典的异常处理问题的解决方案。考虑以下代码:

def foo(n)
  puts " for #{n}"
  sleep n
  raise "after #{n}"
end

begin
  threads = []
  [5, 15, 20, 3].each do |i|
    threads << Thread.new do
      foo(i)
    end
  end

  threads.each(&:join)      
rescue Exception => e
  puts "EXCEPTION: #{e.inspect}"
  puts "MESSAGE: #{e.message}"
end

这段代码在5秒后捕获异常。

但是,如果我将数组更改为[15, 5, 20, 3],那么上面的代码会在15秒后捕获异常。简而言之,它总是捕获第一个线程引发的异常。

有什么想法,为什么会这样?为什么它每次都不在3秒后捕获异常?如何捕获任何线程引发的第一个异常?

5个回答

67
如果你想让任何一个线程中的未处理异常导致解释器退出,你需要将Thread::abort_on_exception=设置为true。未处理的异常会导致线程停止运行。如果你不将这个变量设置为true,异常只会在你调用Thread#joinThread#value时才会被抛出。如果设置为true,它将在发生异常时被抛出并传播到主线程。
Thread.abort_on_exception=true # add this

def foo(n)
    puts " for #{n}"
    sleep n
    raise "after #{n}"
end

begin
    threads = []
    [15, 5, 20, 3].each do |i|
        threads << Thread.new do
            foo(i)
        end
    end
    threads.each(&:join)

rescue Exception => e

    puts "EXCEPTION: #{e.inspect}"
    puts "MESSAGE: #{e.message}"
end

输出:

 for 5
 for 20
 for 3
 for 15
EXCEPTION: #<RuntimeError: after 3>
MESSAGE: after 3

注意:但如果您希望任何特定的线程实例以这种方式引发异常,那么有类似的abort_on_exception = Thread实例方法。请参考abort_on_exception= Thread instance method
t = Thread.new {
   # do something and raise exception
}
t.abort_on_exception = true

谢谢你的回答。 我知道abort_on_exception标志。但我的要求是知道哪个线程首先引发异常,然后做出一些决定。 - Akash Agrawal
所以这是一个测试代码。我的问题是如何捕获任何线程抛出的第一个异常。任何线程随时都可以抛出异常。 - Akash Agrawal
你不需要在 begin-rescue-end 子句中包装整个线程体; 所有的异常将在 Thread#join 处重新引发。 - Franklin Yu
3
Thread.abort_on_exception = true 是一个全局设置,因此请注意它可能会破坏应用程序或其依赖项中预期的默认行为。除非是非常简单的脚本,否则建议使用 t.abort_on_exception = true 方法。请注意不要改变原来的意思。 - Joakim Kolsjö
1
如果这段代码在 gem 中或者将被其他你无法控制的代码调用,那么实例方法也不是很有用,因为你实际上正在向整个运行时强制执行中止行为,而你对其中大部分都没有了解或(应该)控制权。 - Keith Bennett
显示剩余3条评论

6
Thread.class_eval do
  alias_method :initialize_without_exception_bubbling, :initialize
  def initialize(*args, &block)
    initialize_without_exception_bubbling(*args) {
      begin
        block.call
      rescue Exception => e
        Thread.main.raise e
      end
    }
  end
end

9
因为这只是没有解释的代码,所以被投下反对票。需要加上注释、描述它在做什么以及为什么要这样做等内容。 - Tim Holt
2
不要覆盖 Ruby 核心类。-1。 - Senjai
3
我不同意这两种观点。 - Nakilon
1
Thread.main.raise e 可以很好地将异常传递到堆栈上。 - count0

1

延迟异常处理(受@Jason Ling启发)

class SafeThread < Thread

  def initialize(*args, &block)
    super(*args) do
      begin
        block.call
      rescue Exception => e
        @exception = e
      end
    end
  end

  def join
    raise_postponed_exception
    super
    raise_postponed_exception
  end

  def raise_postponed_exception
    Thread.current.raise @exception if @exception
  end

end


puts :start

begin
  thread = SafeThread.new do
    raise 'error from sub-thread'
  end

  puts 'do something heavy before joining other thread'
  sleep 1

  thread.join
rescue Exception => e
  puts "Caught: #{e}"
end

puts 'proper end'

0

这将等待第一个线程引发或返回(并重新引发):

require 'thwait'
def wait_for_first_block_to_complete(*blocks)
  threads = blocks.map do |block|
    Thread.new do
      block.call
    rescue StandardError
      $!
    end
  end
  waiter = ThreadsWait.new(*threads)
  value = waiter.next_wait.value
  threads.each(&:kill)
  raise value if value.is_a?(StandardError)
  value
end

0

Jason Ling的回答将会忽略传递给Thread.new的任何参数。这将破坏Puma和其他宝石。为了避免这个问题,你可以使用:

Thread.class_eval do
  alias_method :initialize_without_exception_bubbling, :initialize
  def initialize(*args, &block)
    initialize_without_exception_bubbling(*args) {
      begin
        block.call(*args)
      rescue Exception => e
        Thread.main.raise e
      end
    }
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接