Поймать исключение потока в потоке вызывающего в Python

Я очень новичок в Python и многопоточном программировании в целом. В принципе, у меня есть script, который скопирует файлы в другое место. Я хотел бы, чтобы это было помещено в другой поток, поэтому я могу вывести ...., чтобы показать, что script все еще работает.

Проблема, с которой я столкнулась, заключается в том, что если файлы не могут быть скопированы, это вызовет исключение. Это нормально, если вы работаете в основном потоке; однако следующий код не работает:

try:
    threadClass = TheThread(param1, param2, etc.)
    threadClass.start()   ##### **Exception takes place here**
except:
    print "Caught an exception"

В самом классе потоков я попытался перебросить исключение, но это не сработает. Я видел, что люди здесь задают похожие вопросы, но все они, кажется, делают что-то более конкретное, чем то, что я пытаюсь сделать (и я не совсем понимаю предлагаемые решения). Я видел, как люди упоминают использование sys.exc_info(), однако я не знаю, где и как его использовать.

Вся помощь очень ценится!

EDIT: Код для класса потока ниже:

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

Ответ 1

Проблема заключается в том, что thread_obj.start() немедленно возвращается. Ребенок, который вы породили, выполняет в своем собственном контексте со своим собственным стеком. Любое исключение происходит в контексте дочернего потока, и оно находится в собственном стеке. Один из способов, которым я могу сейчас подумать, передать эту информацию родительскому потоку, - это использовать какую-то передачу сообщений, чтобы вы могли изучить это.

Попробуйте это для размера:

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 __name__ == '__main__':
    main()

Ответ 2

Вы должны думать о потоках с точки зрения телефонных звонков.

Рассмотрим это.

Вы вызываете локальный городской совет и задаете вопрос. Пока они находят ответ для вас, держитесь. Когда у них будет ответ, они скажут это вам, а затем вы повесьте трубку. Если по какой-то причине они не могут найти ответ (исключение), они скажут вам это.

Так работает синхронный, нормальный вызов метода. Вы вызываете метод, когда он возвращается, у вас есть ответ (хороший или плохой.)

Однако поток выглядит примерно так:

Вы вызываете локальный городской совет и задаете вопрос, и просите их перезвонить вам, когда у них будет ответ. Затем вы вешаете трубку.

На этом этапе вы не знаете, найдут ли они ответ или нет, поэтому любая попытка попытаться обработать результат запроса не удастся, так как у вас просто нет результатов.

Вместо этого вы должны реагировать на входящий вызов и получать новости, хорошие или плохие, а затем обрабатывать их.

В терминах вашего кода вам необходимо иметь код, который реагирует на ваш поток, и регистрирует или обрабатывает исключение. Код, который у вас есть в вашем вопросе, который, как вы говорите, не работает, похож на попытку обработать результаты телефонного звонка сразу после взлома, когда у вас все еще нет ответа.

Ответ 3

Модуль concurrent.futures упрощает работу в отдельных потоках (или процессах) и обрабатывает все возникающие исключения:

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 входит в состав Python 3.2 и доступен как backported futures module для более ранних версий.

Ответ 4

Хотя невозможно напрямую поймать исключение, созданное в другом потоке, здесь код, чтобы достаточно прозрачно получить что-то очень близкое к этой функциональности. Ваш дочерний поток должен подклассифицировать класс ExThread вместо threading.Thread, а родительский поток должен вызывать метод child_thread.join_with_exception() вместо child_thread.join(), ожидая завершения потока.

Технические подробности этой реализации: когда дочерний поток генерирует исключение, он передается родительскому элементу через Queue и снова бросается в родительский поток. Обратите внимание, что в этом подходе нет занятости.

#!/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 __name__ == '__main__':
    main()

Ответ 5

Если в потоке возникает исключение, лучшим способом является повторное повышение его в потоке вызывающего абонента во время join. Вы можете получить информацию об исключении, которое в настоящее время обрабатывается с помощью функции sys.exc_info(). Эта информация может быть просто сохранена как свойство объекта потока до тех пор, пока не будет вызываться join, после чего она может быть повторно поднята.

Обратите внимание, что a Queue.Queue (как предложено в других ответах) не требуется в этом простом случае, когда поток генерирует не более одного исключения и заканчивается сразу после выброса исключения. Мы избегаем условий гонки, просто ожидая завершения потока.

Например, продолжите ExcThread (см. ниже), переопределяя excRun (вместо 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:

Форма 3 аргументов для raise исчезает в Python 3, поэтому измените последнюю строку на:

raise new_exc.with_traceback(self.exc[2])

Ответ 6

На этот вопрос есть много действительно сложных ответов. Я упрощаю это, потому что этого мне кажется достаточно для большинства вещей.

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

Если вы уверены, что вы когда-либо будете работать только на одной или другой версии Python, вы можете сократить метод run() до простой искаженной версии (если вы будете работать только на версиях Python до 3), или просто чистая версия (если вы будете работать только на версиях Python, начинающихся с 3).

Пример использования:

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()

И вы увидите исключение, возникшее в другом потоке, когда вы присоединитесь.

Если вы используете six или только на Python 3, вы можете улучшить информацию трассировки стека, полученную при повторном возникновении исключения. Вместо только стека в точке соединения вы можете заключить внутреннее исключение в новое внешнее исключение и получить обе трассировки стека с помощью

six.raise_from(RuntimeError('Exception in thread'),self.exc)

или

raise RuntimeError('Exception in thread') from self.exc

Ответ 7

Это была неприятная небольшая проблема, и я хотел бы бросить свое решение. Некоторые другие решения, которые я нашел (async.io, например), выглядели многообещающими, но также представили немного черного ящика. Подход очереди/событийный цикл связывает вас с определенной реализацией. Исходный код параллельных фьючерсов, однако, составляет всего 1000 строк и легко понять. Это позволило мне легко решить мою проблему: создать рабочие рабочие потоки без большой настройки и уловить исключения в основном потоке.

В моем решении используется API-интерфейс параллельных фьючерсов и API потоковой передачи. Он позволяет создать рабочего, который даст вам и поток, и будущее. Таким образом, вы можете присоединиться к потоку, чтобы дождаться результата:

worker = Worker(test)
thread = worker.start()
thread.join()
print(worker.future.result())

... или вы можете позволить работнику просто отправить обратный вызов, когда это будет сделано:

worker = Worker(test)
thread = worker.start(lambda x: print('callback', x))

... или вы можете выполнить цикл до завершения события:

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)

Здесь код:

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())

... и тестовая функция:

def test(*args):
    print('args are', args)
    time.sleep(2)
    raise Exception('foo')

Ответ 8

concurrent.futures.as_completed

https://docs.python.org/3.7/library/concurrent.futures.html#concurrent.futures.as_completed

Следующее решение:

  • немедленно возвращается в основной поток при вызове исключения
  • не требует дополнительных пользовательских классов, потому что это не нужно:
    • явная Queue
    • добавить что-то еще, кроме вашего рабочего потока

Источник:

#!/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()))

Возможный вывод:

0
0
1
1
2
2
0
Exception()
1
2
None

К сожалению, невозможно уничтожить фьючерсы, чтобы отменить другие, так как один не получается:

Если вы делаете что-то вроде:

for future in concurrent.futures.as_completed(futures):
    if future.exception() is not None:
        raise future.exception()

затем with ловит его и ждет окончания второго потока, прежде чем продолжить. Следующее ведет себя аналогично:

for future in concurrent.futures.as_completed(futures):
    future.result()

поскольку future.result() повторно вызывает исключение, если оно произошло.

Если вы хотите выйти из всего процесса Python, вы можете использовать os._exit(0), но это, вероятно, означает, что вам нужен рефакторинг.

Пользовательский класс с идеальной семантикой исключений

Я закончил программировать идеальный интерфейс для себя: Правильный способ ограничить максимальное количество одновременно работающих потоков? раздел "Пример очереди с обработкой ошибок". Этот класс призван быть как удобным, так и дать вам полный контроль над отправкой и обработкой результатов/ошибок.

Протестировано на Python 3.6.7, Ubuntu 18.04.

Ответ 9

Как noobie для Threading, мне потребовалось много времени, чтобы понять, как реализовать код Mateusz Kobos (см. выше). Здесь уточненная версия, чтобы понять, как ее использовать.

#!/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 __name__ == '__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

Ответ 10

Подобным образом, как RickardSjogren без очереди, sys и т.д., но также и без некоторых слушателей для сигналов: выполните непосредственно обработчик исключений, который соответствует исключающему блоку.

#!/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

Только self._callback и исключающий блок в run() являются дополнительными к обычной threading.Thread.

Ответ 11

Я знаю, что немного опоздал на вечеринку здесь, но у меня была очень похожая проблема, но она включала использование tkinter в качестве графического интерфейса, и основной цикл лишил возможности использовать любое из решений, которые зависят от.join(). Поэтому я адаптировал решение, данное в РЕДАКТИРЕ исходного вопроса, но сделал его более общим, чтобы его было легче понять другим.

Вот новый класс потока в действии:

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 __name__ == "__main__":
    input = 'useful'

    t1 = ExceptionThread(target=test_function_1, args=[input])
    t1.start()

Конечно, вы всегда можете обработать исключение другим способом, например, распечатать его или вывести на консоль.

Это позволяет вам использовать класс ExceptionThread точно так же, как и класс Thread, без каких-либо специальных изменений.

Ответ 12

Использование голых исключений не является хорошей практикой, потому что вы обычно поймаете больше, чем вы торгуетесь.

Я бы предложил изменить except, чтобы поймать ТОЛЬКО исключение, которое вы хотели бы обработать. Я не думаю, что повышение его имеет желаемый эффект, потому что когда вы идете создавать экземпляр TheThread во внешнем try, если он вызывает исключение, назначение никогда не произойдет.

Вместо этого вы можете просто предупредить об этом и двигаться дальше, например:

def run(self):
    try:
       shul.copytree(self.sourceFolder, self.destFolder)
    except OSError, err:
       print err

Затем, когда это исключение поймано, вы можете его обработать. Затем, когда внешний try ловит исключение из TheThread, вы знаете, что это не тот, который вы уже обработали, и поможет вам изолировать поток процессов.

Ответ 13

Один метод, который я люблю, основан на шаблоне наблюдателя. Я определяю класс сигнала, который использует мой поток, чтобы испускать исключения для слушателей. Он также может использоваться для возврата значений из потоков. Пример:

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 __name__ == '__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'

У меня недостаточно опыта работы с потоками, чтобы утверждать, что это абсолютно безопасный метод. Но это сработало для меня, и мне нравится гибкость.

Ответ 14

Простым способом обхода исключения потока и передачи назад методу вызывающего может быть передача словаря или списка в метод 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'])

Ответ 15

Wrap Thread с хранилищем исключений.

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()