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?
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
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
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'
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