web-dev-qa-db-ja.com

Pythonの呼び出し元スレッドでスレッドの例外をキャッチします

私はPythonおよびマルチスレッドプログラミング全般に非常に不慣れです。基本的に、ファイルを別の場所にコピーするスクリプトがあります。これを別のスレッドに配置して、....を出力して、スクリプトがまだ実行中であることを示すことができるようにします。

私が抱えている問題は、ファイルをコピーできない場合、例外がスローされることです。メインスレッドで実行している場合、これは問題ありません。ただし、次のコードを使用しても機能しません。

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
165
Phanto

問題は、thread_obj.start()がすぐに戻ることです。生成した子スレッドは、独自のスタックで独自のコンテキストで実行されます。そこで発生する例外はすべて、子スレッドのコンテキストにあり、独自のスタックにあります。この情報を親スレッドに伝えるために今考えることができる方法の1つは、何らかのメッセージの受け渡しを使用することです。

サイズについてはこれを試してください:

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

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に含まれており、以前のバージョンでは バックポートfuturesモジュール として利用できます。

32
Jon-Eric

別のスレッドでスローされた例外を直接キャッチすることはできませんが、この機能に非常に近いものを非常に透過的に取得するコードを次に示します。子スレッドは、threading.Threadの代わりにExThreadクラスをサブクラス化する必要があり、親スレッドは、スレッドがジョブを終了するのを待つときに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 __== '__main__':
    main()
27
Mateusz Kobos

この質問には本当に奇妙に複雑な答えがたくさんあります。私はこれを単純化しすぎていますか?.

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より前)、または単にクリーンバージョン(3以降のPythonのバージョンでのみ実行する場合)。

使用例:

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

また、参加すると、他のスレッドで例外が発生します。

17
ArtOfWarfare

これは厄介な小さな問題であり、私は自分の解決策を投入したいと思います。私が見つけた他の解決策(たとえばasync.io)は有望に見えましたが、少し黒い箱を提示しました。キュー/イベントループアプローチは、ある種の実装に結び付けます。 ただし、同時フューチャのソースコードは約1000行であり、理解しやすい 。これにより、問題を簡単に解決できました。多くのセットアップなしでアドホックワーカースレッドを作成し、メインスレッドで例外をキャッチできるようになりました。

私のソリューションでは、concurrent futures APIとthreading 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')
4
Calvin Froedge

concurrent.futures.as_completed

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

次の解決策:

  • 例外が呼び出されるとすぐにメインスレッドに戻る
  • 以下を必要としないため、追加のユーザー定義クラスは必要ありません。
    • 明示的なQueue
    • 作業スレッドの周囲にelseを追加するには

ソース:

#!/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はそれをキャッチし、2番目のスレッドが終了するのを待ってから続行します。以下は同様に動作します。

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

future.result()は例外が発生した場合に例外を再度発生させるためです。

Pythonプロセス全体を終了する場合は、 os._exit(0) で済ますことができますが、これはおそらくリファクタリングが必要であることを意味します。

完全な例外セマンティクスを備えたカスタムクラス

一度に実行するスレッドの最大数を制限する正しい方法? セクション「エラー処理を備えたキューの例」で、自分にぴったりのインターフェイスをコーディングしました。このクラスは、便利であり、送信と結果/エラー処理を完全に制御することを目的としています。

Python 3.6.7、Ubuntu 18.04でテスト済み。

スレッディングの初心者として、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 __== '__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
2
RightmireM

私はここでパーティーに少し遅れていることを知っていますが、私は非常に似た問題を抱えていましたが、tkinterをGUIとして使用することを含んでおり、メインループは.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 __== "__main__":
    input = 'useful'

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

もちろん、ログからの例外を印刷する、コンソールに出力するなど、他の方法で例外をいつでも処理させることができます。

これにより、特別な変更を加えることなく、Threadクラスとまったく同じようにExceptionThreadクラスを使用できます。

1
Firo

RickardSjogrenのQueueや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()のexceptブロックのみが通常のthreading.Threadに追加されます。

1
Chickenmarkus

裸の例外を使用することは、通常は思いがけないほど多くをキャッチするため、良い習慣ではありません。

exceptを変更して、処理したい例外のみをキャッチすることをお勧めします。外側のTheThreadをインスタンス化するときにtryをインスタンス化すると、例外が発生した場合、割り当てが発生しなくなるため、それを上げると望ましい効果があるとは思いません。

代わりに、次のようにアラートを発して先に進むことができます。

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

その後、その例外がキャッチされると、そこで例外を処理できます。次に、外側のtryTheThreadから例外をキャッチすると、それが既に処理したものではないことがわかり、プロセスフローを分離するのに役立ちます。

0
jathanism

私が好きな方法の1つは、 observer pattern に基づいています。スレッドがリスナーに例外を発行するために使用するシグナルクラスを定義します。スレッドから値を返すためにも使用できます。例:

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'

私はこれが完全に安全な方法であると主張するのに十分なスレッドの操作経験がありません。しかし、それは私のために働いており、柔軟性が好きです。

0
RickardSjogren

例外ストレージでスレッドをラップします。

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

スレッドの例外をキャッチして呼び出し元のメソッドとやり取りする簡単な方法は、workerメソッドに辞書またはリストを渡すことです。

例(辞書を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'])
0