web-dev-qa-db-fra.com

Gestion des exceptions déclenchées dans un fil Ruby

Je cherche une solution au problème classique de la gestion des exceptions. Considérons le morceau de code suivant:

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

Ce code intercepte l'exception après 5 secondes.

Mais si je change le tableau en [15, 5, 20, 3], le code ci-dessus intercepte l’exception après 15 secondes. En bref, il intercepte toujours l'exception levée dans le premier thread.

Toute idée, pourquoi donc. Pourquoi ne détecte-t-il pas l'exception après 3 secondes à chaque fois? Comment attraper la première exception déclenchée par un thread?

28
Akash Agrawal

Si vous souhaitez qu'une exception non gérée dans un thread entraîne la sortie de l'interpréteur, vous devez définir Thread :: abort_on_exception = à true. Une exception non gérée provoque l’arrêt du thread. Si vous ne définissez pas cette variable sur true, une exception ne sera déclenchée que lorsque vous appelez Thread#join ou Thread#value pour le thread. Si défini à true, il sera déclenché lorsqu'il se produira et se propagera au thread principal.

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

Sortie:

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

Remarque: mais si vous souhaitez qu'une exception de thread particulière lève une exception de cette manière, il existe des méthodes similaires abort_on_exception = Méthode d'instance de thread :

t = Thread.new {
   # do something and raise exception
}
t.abort_on_exception = true
54
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
6
Jason Ling

Traitement des exceptions reportées (inspiré de @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
Dan Key

Cela attendra que le premier thread monte ou revienne (et re-relance):

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
grosser