Je suis très novice en Python et en programmation multithread en général. En gros, j'ai un script qui va copier les fichiers dans un autre endroit. Je voudrais que cela soit placé dans un autre thread afin que je puisse sortir ....
pour indiquer que le script est toujours en cours d'exécution.
Le problème que je rencontre est que si les fichiers ne peuvent pas être copiés, une exception sera levée. C'est ok si vous exécutez dans le thread principal; Cependant, le code suivant ne fonctionne pas:
try:
threadClass = TheThread(param1, param2, etc.)
threadClass.start() ##### **Exception takes place here**
except:
print "Caught an exception"
Dans la classe de thread elle-même, j'ai essayé de relancer l'exception, mais cela ne fonctionne pas. J'ai vu des gens ici poser des questions similaires, mais ils semblent tous faire quelque chose de plus spécifique que ce que j'essaie de faire (et je ne comprends pas très bien les solutions proposées). J'ai vu des gens mentionner l'utilisation de sys.exc_info()
, mais je ne sais pas où et comment l'utiliser.
Toute aide est grandement appréciée!
EDIT: Le code de la classe de thread est ci-dessous:
class TheThread(threading.Thread):
def __init__(self, sourceFolder, destFolder):
threading.Thread.__init__(self)
self.sourceFolder = sourceFolder
self.destFolder = destFolder
def run(self):
try:
shul.copytree(self.sourceFolder, self.destFolder)
except:
raise
Le problème est que thread_obj.start()
retourne immédiatement. Le thread enfant que vous avez créé s'exécute dans son propre contexte, avec sa propre pile. Toute exception qui s'y produit se trouve dans le contexte du thread enfant et se trouve dans sa propre pile. Une façon dont je peux penser en ce moment pour communiquer ces informations au thread parent consiste à utiliser une sorte de message de transmission, afin que vous puissiez examiner cela.
Essayez ceci pour la taille:
import sys
import threading
import Queue
class ExcThread(threading.Thread):
def __init__(self, bucket):
threading.Thread.__init__(self)
self.bucket = bucket
def run(self):
try:
raise Exception('An error occured here.')
except Exception:
self.bucket.put(sys.exc_info())
def main():
bucket = Queue.Queue()
thread_obj = ExcThread(bucket)
thread_obj.start()
while True:
try:
exc = bucket.get(block=False)
except Queue.Empty:
pass
else:
exc_type, exc_obj, exc_trace = exc
# deal with the exception
print exc_type, exc_obj
print exc_trace
thread_obj.join(0.1)
if thread_obj.isAlive():
continue
else:
break
if __== '__main__':
main()
Le module concurrent.futures
simplifie le travail dans des threads (ou processus) distincts et permet de gérer les exceptions résultantes:
import concurrent.futures
import shutil
def copytree_with_dots(src_path, dst_path):
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Execute the copy on a separate thread,
# creating a future object to track progress.
future = executor.submit(shutil.copytree, src_path, dst_path)
while future.running():
# Print pretty dots here.
pass
# Return the value returned by shutil.copytree(), None.
# Raise any exceptions raised during the copy process.
return future.result()
concurrent.futures
est inclus dans Python 3.2 et est disponible sous la forme/ le module futures
backported pour les versions antérieures.
Bien qu'il ne soit pas possible d'intercepter directement une exception générée dans un autre thread, voici un code permettant d'obtenir de manière assez transparente quelque chose de très proche de cette fonctionnalité. Votre thread enfant doit sous-classer la classe ExThread
au lieu de threading.Thread
et le thread parent doit appeler la méthode child_thread.join_with_exception()
au lieu de child_thread.join()
lorsqu'il attend que le thread termine son travail.
Détails techniques de cette implémentation: lorsque le thread enfant lève une exception, celle-ci est transmise au parent par le biais d'une Queue
, puis renvoyée dans le thread parent. Notez qu'il n'y a pas d'attente occupée dans cette approche.
#!/usr/bin/env python
import sys
import threading
import Queue
class ExThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.__status_queue = Queue.Queue()
def run_with_exception(self):
"""This method should be overriden."""
raise NotImplementedError
def run(self):
"""This method should NOT be overriden."""
try:
self.run_with_exception()
except BaseException:
self.__status_queue.put(sys.exc_info())
self.__status_queue.put(None)
def wait_for_exc_info(self):
return self.__status_queue.get()
def join_with_exception(self):
ex_info = self.wait_for_exc_info()
if ex_info is None:
return
else:
raise ex_info[1]
class MyException(Exception):
pass
class MyThread(ExThread):
def __init__(self):
ExThread.__init__(self)
def run_with_exception(self):
thread_name = threading.current_thread().name
raise MyException("An error in thread '{}'.".format(thread_name))
def main():
t = MyThread()
t.start()
try:
t.join_with_exception()
except MyException as ex:
thread_name = threading.current_thread().name
print "Caught a MyException in thread '{}': {}".format(thread_name, ex)
if __== '__main__':
main()
Si une exception se produit dans un thread, le meilleur moyen consiste à le relancer dans le thread de l'appelant au cours de join
. Vous pouvez obtenir des informations sur l'exception en cours de traitement à l'aide de la fonction sys.exc_info()
. Ces informations peuvent simplement être stockées en tant que propriété de l'objet thread jusqu'à ce que join
soit appelé. À ce stade, il peut être à nouveau généré.
Notez qu'un Queue.Queue
(comme suggéré dans d'autres réponses) n'est pas nécessaire dans ce cas simple où le thread lève au plus 1 exception et se termine juste après le lancement d'une exception. Nous évitons les conditions de concurrence en attendant simplement que le fil soit terminé.
Par exemple, étendez ExcThread
(ci-dessous) en remplaçant excRun
(au lieu de run
).
Python 2.x:
import threading
class ExcThread(threading.Thread):
def excRun(self):
pass
def run(self):
self.exc = None
try:
# Possibly throws an exception
self.excRun()
except:
import sys
self.exc = sys.exc_info()
# Save details of the exception thrown but don't rethrow,
# just complete the function
def join(self):
threading.Thread.join(self)
if self.exc:
msg = "Thread '%s' threw an exception: %s" % (self.getName(), self.exc[1])
new_exc = Exception(msg)
raise new_exc.__class__, new_exc, self.exc[2]
Python 3.x:
La forme à 3 arguments pour raise
a disparu de Python 3, remplacez la dernière ligne par:
raise new_exc.with_traceback(self.exc[2])
Il y a beaucoup de réponses vraiment étrangement compliquées à cette question. Est-ce que je simplifie trop, parce que cela me semble suffisant pour la plupart des choses.
from threading import Thread
class PropagatingThread(Thread):
def run(self):
self.exc = None
try:
if hasattr(self, '_Thread__target'):
# Thread uses name mangling prior to Python 3.
self.ret = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
else:
self.ret = self._target(*self._args, **self._kwargs)
except BaseException as e:
self.exc = e
def join(self):
super(PropagatingThread, self).join()
if self.exc:
raise self.exc
return self.ret
Si vous êtes certain de ne jamais utiliser que l'une ou l'autre des versions de Python, vous pouvez réduire la méthode run()
à la version endommagée (si vous n'exécutez que des versions de Python antérieures à la version 3), ou juste la version propre (si vous ne vous exécuterez que sur les versions de Python commençant par 3).
Exemple d'utilisation:
def f(*args, **kwargs)
print(args)
print(kwargs)
raise Exception('I suck')
t = PropagatingThread(target=f, args=(5,), kwargs={'hello':'world'})
t.start()
t.join()
Et vous verrez l'exception levée sur l'autre thread lorsque vous rejoindrez.
C’était un vilain petit problème, et j’aimerais ajouter ma solution. Certaines autres solutions que j’ai trouvées (async.io par exemple) semblaient prometteuses mais présentaient aussi un peu une boîte noire. L'approche de la boucle file d'attente/événement vous lie à une certaine implémentation. Le code source des contrats à terme simultanés ne représente toutefois qu’environ 1000 lignes et est facile à comprendre . Cela me permettait de résoudre facilement mon problème: créer des threads de travail ad-hoc sans trop de configuration et pouvoir intercepter des exceptions dans le thread principal.
Ma solution utilise les API simultanée Futures et API de threading. Il vous permet de créer un ouvrier qui vous donne à la fois le fil et le futur. De cette façon, vous pouvez rejoindre le fil pour attendre le résultat:
worker = Worker(test)
thread = worker.start()
thread.join()
print(worker.future.result())
... ou vous pouvez laisser le travailleur envoyer un rappel lorsque vous avez terminé:
worker = Worker(test)
thread = worker.start(lambda x: print('callback', x))
... ou vous pouvez boucler jusqu'à la fin de l'événement:
worker = Worker(test)
thread = worker.start()
while True:
print("waiting")
if worker.future.done():
exc = worker.future.exception()
print('exception?', exc)
result = worker.future.result()
print('result', result)
break
time.sleep(0.25)
Voici le code:
from concurrent.futures import Future
import threading
import time
class Worker(object):
def __init__(self, fn, args=()):
self.future = Future()
self._fn = fn
self._args = args
def start(self, cb=None):
self._cb = cb
self.future.set_running_or_notify_cancel()
thread = threading.Thread(target=self.run, args=())
thread.daemon = True #this will continue thread execution after the main thread runs out of code - you can still ctrl + c or kill the process
thread.start()
return thread
def run(self):
try:
self.future.set_result(self._fn(*self._args))
except BaseException as e:
self.future.set_exception(e)
if(self._cb):
self._cb(self.future.result())
... et la fonction de test:
def test(*args):
print('args are', args)
time.sleep(2)
raise Exception('foo')
concurrent.futures.as_completed
https://docs.python.org/3.7/library/concurrent.futures.html#concurrent.futures.as_completed
La solution suivante:
Queue
expliciteLa source:
#!/usr/bin/env python3
import concurrent.futures
import time
def func_that_raises(do_raise):
for i in range(3):
print(i)
time.sleep(0.1)
if do_raise:
raise Exception()
for i in range(3):
print(i)
time.sleep(0.1)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
futures = []
futures.append(executor.submit(func_that_raises, False))
futures.append(executor.submit(func_that_raises, True))
for future in concurrent.futures.as_completed(futures):
print(repr(future.exception()))
Sortie possible:
0
0
1
1
2
2
0
Exception()
1
2
None
Il n'est malheureusement pas possible de tuer des contrats à terme pour annuler les autres en cas d'échec:
concurrent.features
; Python: concurrent.futures Comment le rendre annulable?threading
: Est-il possible de tuer un fil?Si vous faites quelque chose comme:
for future in concurrent.futures.as_completed(futures):
if future.exception() is not None:
raise future.exception()
alors la with
l'attrape et attend que le deuxième thread se termine avant de continuer. Le comportement suivant est similaire:
for future in concurrent.futures.as_completed(futures):
future.result()
depuis future.result()
, une exception est déclenchée à nouveau.
Si vous souhaitez quitter tout le processus Python, vous pouvez vous en tirer avec os._exit(0)
, mais cela signifie probablement que vous avez besoin d'un refactor.
Testé sur Python 3.6.7, Ubuntu 18.04.
En tant que novice de Threading, il m'a fallu beaucoup de temps pour comprendre comment implémenter le code de Mateusz Kobos (ci-dessus). Voici une version clarifiée pour aider à comprendre comment l’utiliser.
#!/usr/bin/env python
import sys
import threading
import Queue
class ExThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.__status_queue = Queue.Queue()
def run_with_exception(self):
"""This method should be overriden."""
raise NotImplementedError
def run(self):
"""This method should NOT be overriden."""
try:
self.run_with_exception()
except Exception:
self.__status_queue.put(sys.exc_info())
self.__status_queue.put(None)
def wait_for_exc_info(self):
return self.__status_queue.get()
def join_with_exception(self):
ex_info = self.wait_for_exc_info()
if ex_info is None:
return
else:
raise ex_info[1]
class MyException(Exception):
pass
class MyThread(ExThread):
def __init__(self):
ExThread.__init__(self)
# This overrides the "run_with_exception" from class "ExThread"
# Note, this is where the actual thread to be run lives. The thread
# to be run could also call a method or be passed in as an object
def run_with_exception(self):
# Code will function until the int
print "sleeping 5 seconds"
import time
for i in 1, 2, 3, 4, 5:
print i
time.sleep(1)
# Thread should break here
int("str")
# I'm honestly not sure why these appear here? So, I removed them.
# Perhaps Mateusz can clarify?
# thread_name = threading.current_thread().name
# raise MyException("An error in thread '{}'.".format(thread_name))
if __== '__main__':
# The code lives in MyThread in this example. So creating the MyThread
# object set the code to be run (but does not start it yet)
t = MyThread()
# This actually starts the thread
t.start()
print
print ("Notice 't.start()' is considered to have completed, although"
" the countdown continues in its new thread. So you code "
"can tinue into new processing.")
# Now that the thread is running, the join allows for monitoring of it
try:
t.join_with_exception()
# should be able to be replace "Exception" with specific error (untested)
except Exception, e:
print
print "Exceptioon was caught and control passed back to the main thread"
print "Do some handling here...or raise a custom exception "
thread_name = threading.current_thread().name
e = ("Caught a MyException in thread: '" +
str(thread_name) +
"' [" + str(e) + "]")
raise Exception(e) # Or custom class of exception, such as MyException
De la même manière que RickardSjogren sans Queue, sys, etc., mais aussi sans quelques écouteurs de signaux: exécuter directement un gestionnaire d'exceptions qui correspond à un bloc except.
#!/usr/bin/env python3
import threading
class ExceptionThread(threading.Thread):
def __init__(self, callback=None, *args, **kwargs):
"""
Redirect exceptions of thread to an exception handler.
:param callback: function to handle occured exception
:type callback: function(thread, exception)
:param args: arguments for threading.Thread()
:type args: Tuple
:param kwargs: keyword arguments for threading.Thread()
:type kwargs: dict
"""
self._callback = callback
super().__init__(*args, **kwargs)
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except BaseException as e:
if self._callback is None:
raise e
else:
self._callback(self, e)
finally:
# Avoid a refcycle if the thread is running a function with
# an argument that has a member that points to the thread.
del self._target, self._args, self._kwargs, self._callback
Seul self._callback et le bloc except de run () s'ajoutent à threading.Thread normal.
L'utilisation d'exceptions nues n'est pas une bonne pratique car vous attrapez généralement plus que ce que vous négociez.
Je suggérerais de modifier la except
pour ne saisir que l'exception que vous voudriez gérer. Je ne pense pas que le relancer produise l'effet escompté, car si vous instanciez TheThread
dans le try
extérieur, s'il y a une exception, l'assignation ne se produira jamais.
Au lieu de cela, vous voudrez peut-être simplement alerter et passer à autre chose, par exemple:
def run(self):
try:
shul.copytree(self.sourceFolder, self.destFolder)
except OSError, err:
print err
Ensuite, lorsque cette exception est interceptée, vous pouvez la gérer ici. Ensuite, lorsque la variable try
extérieure intercepte une exception de TheThread
, vous savez que ce ne sera pas celle que vous avez déjà gérée et vous aidera à isoler votre flux de processus.
Une méthode que j'aime beaucoup est basée sur le motif observer . Je définis une classe de signal que mon thread utilise pour émettre des exceptions aux auditeurs. Il peut également être utilisé pour renvoyer des valeurs de threads. Exemple:
import threading
class Signal:
def __init__(self):
self._subscribers = list()
def emit(self, *args, **kwargs):
for func in self._subscribers:
func(*args, **kwargs)
def connect(self, func):
self._subscribers.append(func)
def disconnect(self, func):
try:
self._subscribers.remove(func)
except ValueError:
raise ValueError('Function {0} not removed from {1}'.format(func, self))
class WorkerThread(threading.Thread):
def __init__(self, *args, **kwargs):
super(WorkerThread, self).__init__(*args, **kwargs)
self.Exception = Signal()
self.Result = Signal()
def run(self):
if self._Thread__target is not None:
try:
self._return_value = self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
except Exception as e:
self.Exception.emit(e)
else:
self.Result.emit(self._return_value)
if __== '__main__':
import time
def handle_exception(exc):
print exc.message
def handle_result(res):
print res
def a():
time.sleep(1)
raise IOError('a failed')
def b():
time.sleep(2)
return 'b returns'
t = WorkerThread(target=a)
t2 = WorkerThread(target=b)
t.Exception.connect(handle_exception)
t2.Result.connect(handle_result)
t.start()
t2.start()
print 'Threads started'
t.join()
t2.join()
print 'Done'
Je n'ai pas suffisamment d'expérience de travail avec les threads pour prétendre qu'il s'agit d'une méthode totalement sûre. Mais cela a fonctionné pour moi et j'aime la flexibilité.
Je sais que je suis un peu en retard pour la fête ici, mais je rencontrais un problème très similaire, mais cela impliquait d'utiliser tkinter en tant qu'interface graphique, et mainloop rendait impossible l'utilisation de solutions dépendantes de .join (). J'ai donc adapté la solution donnée dans l'EDIT de la question initiale, mais en la rendant plus générale pour la rendre plus compréhensible pour les autres.
Voici la nouvelle classe de thread en action:
import threading
import traceback
import logging
class ExceptionThread(threading.Thread):
def __init__(self, *args, **kwargs):
threading.Thread.__init__(self, *args, **kwargs)
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except Exception:
logging.error(traceback.format_exc())
def test_function_1(input):
raise IndexError(input)
if __== "__main__":
input = 'useful'
t1 = ExceptionThread(target=test_function_1, args=[input])
t1.start()
Bien sûr, vous pouvez toujours lui demander de gérer l'exception d'une autre manière, par exemple en l'imprimant ou en l'envoyant vers la console.
Cela vous permet d'utiliser la classe ExceptionThread exactement comme vous le feriez avec la classe Thread, sans aucune modification particulière.
Enroulez le fil avec un stockage d'exception.
import threading
import sys
class ExcThread(threading.Thread):
def __init__(self, target, args = None):
self.args = args if args else []
self.target = target
self.exc = None
threading.Thread.__init__(self)
def run(self):
try:
self.target(*self.args)
raise Exception('An error occured here.')
except Exception:
self.exc=sys.exc_info()
def main():
def hello(name):
print(!"Hello, {name}!")
thread_obj = ExcThread(target=hello, args=("Jack"))
thread_obj.start()
thread_obj.join()
exc = thread_obj.exc
if exc:
exc_type, exc_obj, exc_trace = exc
print(exc_type, ':',exc_obj, ":", exc_trace)
main()
Un moyen simple d'attraper l'exception du thread et de communiquer avec la méthode de l'appelant peut être de passer un dictionnaire ou une liste à la méthode worker
.
Exemple (passage du dictionnaire à la méthode worker):
import threading
def my_method(throw_me):
raise Exception(throw_me)
def worker(shared_obj, *args, **kwargs):
try:
shared_obj['target'](*args, **kwargs)
except Exception as err:
shared_obj['err'] = err
shared_obj = {'err':'', 'target': my_method}
throw_me = "Test"
th = threading.Thread(target=worker, args=(shared_obj, throw_me), kwargs={})
th.start()
th.join()
if shared_obj['err']:
print(">>%s" % shared_obj['err'])