【问题标题】:Rescue simultaneous errors raised inside threads拯救线程内引发的同时错误
【发布时间】:2017-06-03 19:32:35
【问题描述】:

使用以下脚本

threads = [
  Thread.new { Thread.current.abort_on_exception = true; raise 'err' },
  Thread.new { Thread.current.abort_on_exception = true; raise 'err' },
]

begin
  threads.each(&:join)
rescue RuntimeError
  puts "Got Error"
end

一半的时间我得到预期的“错误”退出 0,另一半得到test.rb:3:in block in <main>': err (RuntimeError)

救援不应该能够处理这个吗?如果不是,两个线程同时引发错误的替代解决方案是什么?

我考虑过不使用abort_on_exception = true,但问题是如果第一个线程在raise 之前有sleep(10),那么第二个线程会立即出错,直到10 秒才会被捕获向上(由于threads 数组的顺序)。

Ruby MRI 版本: ruby 2.4.0p0(2016-12-24 修订版 57164)[x86_64-darwin15]

任何想法将不胜感激。谢谢!

更新

jruby-9.1.6.0 好像没有这个问题。可能是因为它固有的线程安全性。它总是打印Got Error,没有任何例外。不幸的是,我们不能选择 JRuby。

【问题讨论】:

  • 有趣的是,当所有东西都包裹在 begin/rescue 块中时,它甚至会失败。

标签: ruby multithreading exception


【解决方案1】:

这里有一些拼图。


首先,程序只等待主线程完成:

Thread.new { Thread.current.abort_on_exception = true; raise 'Oh, no!' }

puts 'Ready or not, here I come'

以上可能会或可能不会引发错误。


其次,如果您加入一个线程,则该线程引发的异常会被加入的线程从#join 方法重新引发:

gollum = Thread.new { raise 'My precious!!!' }

begin
  gollum.join
rescue => e
  # Prints 'My precious!!!'
  puts e.message
end

此时,执行将返回到加入的线程。它不再连接到导致错误的线程或任何其他线程。它没有加入其他线程的原因是因为您一次只能加入一个线程。 threads.each(&:join) 实际上将你加入第一个,当它结束时 - 加入第二个等等:

frodo = Thread.new { raise 'Oh, no, Frodo!' }
sam   = Thread.new { raise 'Oh, no, Sam!' }

begin
  [frodo, sam].each(&:join)
rescue => e
  puts e.message
end

puts 'This is the end, my only friend, the end.'

以上印刷品

哦,不,佛罗多!
这是结束,我唯一的朋友,结束。


现在让我们把它放在一起:

frodo = Thread.new { Thread.current.abort_on_exception = true; raise 'Oh, no, Frodo!' }
sam   = Thread.new { Thread.current.abort_on_exception = true; raise 'Oh, no, Sam!' }

begin
  [frodo, sam].each(&:join)
rescue => e
  puts e.message
end

puts 'This is the end, my only friend, the end.'

这里可以发生很多事情。重要的是,如果我们设法加入(在此之前我们没有收到错误),救援将在主线程中捕获异常,无论哪个线程首先设法引发异常,然后在救援后继续。之后,主线程(以及程序)可能会或可能不会在其他线程引发其异常之前完成。


让我们检查一些可能的输出:

ex.rb:1:in `block in ':哦,不,佛罗多! (运行时错误)

Frodo 在我们加入之前提出了他的例外。

哦,不,山姆!
这是结束,我唯一的朋友,结束。

我们加入后,Sam 是第一个提出错误的人。在我们在主线程中打印了错误信息之后,我们也打印了结尾。然后主线程完成,在佛罗多提出他的错误之前。

哦,不,Frodo!ex.rb:2:in `block in':哦,不,Sam! (运行时错误)

我们设法加入了。佛罗多是第一个提出的,我们救了出来并打印了。在我们打印完结尾之前,Sam 提出了。

哦,不,山姆!
这是结束,我唯一的朋友,end.ex.rb:1:in `block in':哦,不,佛罗多! (运行时错误)

(很少)我们设法进行了救援。 Sam 首先提出了一个错误,我们从主线程打印了它。我们打印了结尾。就在打印之后,但在主线程终止之前,佛罗多也设法纠正了他的错误。


至于可能的解决方案,您只需要尽可能多的救援,因为有可能引发的线程。请注意,我还将线程创建放在受保护的块中,以确保我们在加入之前也能捕捉到潜在的错误:

def execute_safely_concurrently(number_of_threads, &work)
  return if number_of_threads.zero?

  begin
    Thread.new(&work).join
  rescue => e
    puts e
  end

  execute_safely_concurrently(number_of_threads.pred, &work)
end

execute_safely_concurrently(2) do
  Thread.current.abort_on_exception = true
  raise 'Handle me, bitte!'
end

【讨论】:

  • 感谢您就该问题展开讨论。这些例子很清楚。我的输出似乎总是您上述可能输出的第二和第三(大约 50/50)。我仍在尝试在 MRI 中找到解决方法(JRuby 似乎没有问题)
  • @nolanpro,您的情况完全相同,我只是添加了更多打印件,以便更容易理解正在发生的事情。如果其中一个线程在您到达join 之前引发错误,则会收到错误消息。如果在join 之前没有引发错误并且程序在救援后直接退出,您会得到Got Error。如果在 join 之前没有引发错误,并且在程序即将退出时在救援之后引发第二个错误,那么您很少会同时遇到这两个错误。
  • 如果您仍然不相信,请尝试将sleep 5 放在几个地方。如果您将它直接放在线程创建之后和 begin 块之前,您应该始终得到错误。这是因为在您到达联接之前,其中一个线程有足够的时间来引发。
  • 取而代之的是,您可以将睡眠放在程序的末尾(在关闭 end 之后)。您将始终不会只收到Got Error。原因是在您到达join 之前,要么一个线程会引发(也就是你只得到错误),或者你拯救了一个错误,然后另一个线程也有足够的时间来引发(也就是你得到两个打印和错误)。
  • @nolanpro,也添加了解决方案。
【解决方案2】:

在查看了@ndn 将每个线程包装在它自己的救援中的想法之后。看起来这可以解决问题。这是他修改后的示例,它不会阻止连接时的执行。

@threads = []
def execute_safely_concurrently(&work)
  begin
    @threads << Thread.new(&work)
  rescue RuntimeError => e
    puts "Child Thread Rescue: #{e}"
  end
end

execute_safely_concurrently do
  Thread.current.abort_on_exception = true
  sleep(3)
  raise 'Handle me, bitte 1!'
end

execute_safely_concurrently do
  Thread.current.abort_on_exception = true
  raise 'Handle me, bitte 2!'
end

begin
  @threads.each(&:join)
rescue RuntimeError => e
  puts "Main Thread Rescue: #{e}"
end

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-12-15
    • 1970-01-01
    • 2016-06-21
    • 2013-11-27
    • 2015-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多