处理 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#join
或 Thread#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 线程中引发的异常的主要内容,如果未能解决你的问题,请参考以下文章