处理 Ruby 线程中引发的异常

Posted

技术标签:

【中文标题】处理 Ruby 线程中引发的异常【英文标题】:Handling exceptions raised in a Ruby thread 【发布时间】:2012-02-24 02:28:39 【问题描述】:

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

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 秒后都没有捕捉到异常?如何捕获任何线程引发的第一个异常?

【问题讨论】:

【参考方案1】:

如果您希望任何线程中的任何未处理异常导致解释器退出,您需要将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 instance method:

t = Thread.new 
   # do something and raise exception

t.abort_on_exception = true

【讨论】:

感谢您的回答。我知道 abort_on_exception 标志。但我的要求是知道哪个是第一个引发异常的线程,然后对其做出一些决定。 @AkashAgrawal,我没有得到你最后的评论。您将在rescue 子句中捕获第一个异常(来自具有睡眠 3 的线程),在这里您可以做出决定。如果您的主线程在第一个异常后没有退出,则所有其余线程将继续运行。 所以这是一个测试代码。我的问题是如何捕获从任何线程抛出的第一个异常。任何线程都可以随时抛出异常。 Thread.abort_on_exception = true 是一个全局设置,因此请注意它可能会破坏应用程序中的其他代码,或者它的依赖项需要默认行为。我会选择t.abort_on_exception = true 方法,除非它是一个非常小的脚本。 如果此代码位于 gem 中,或者将被您无法控制的代码调用,那么实例方法也不是很有帮助,因为您实际上是在将中止行为指定给整个运行时,大多数情况下您不知道或(应该)控制的。【参考方案2】:
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

【讨论】:

投了反对票,因为这只是没有解释的代码。需要 cmets,描述它的作用、原因等。 也不要覆盖 ruby​​ 核心类。 -1'd 我不同意这两个 cmets。 Thread.main.raise e 可以很好地将异常向上传播到堆栈。【参考方案3】:

延迟的异常处理(受@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'

【讨论】:

【参考方案4】:

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

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

【讨论】:

【参考方案5】:

Jason Ling's answer 将丢失任何传递给 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

【讨论】:

以上是关于处理 Ruby 线程中引发的异常的主要内容,如果未能解决你的问题,请参考以下文章

如何强制 WCF 线程中未处理的异常使进程崩溃?

C# 中是不是有在给定线程上引发异常的好方法

您可以在不同的线程上重新引发 .NET 异常吗?

捕获不同线程中引起的异常[重复]

C++11 线程是可连接的,但 join() 会引发异常

发布后单用户补丁时,jmeter引发线程异常