在调用线程中捕获线程的异常?

300 投票
22 回答
310804 浏览
提问于 2025-04-15 22:40

我刚开始学习Python和多线程编程。简单来说,我有一个脚本可以把文件复制到另一个地方。我想把这个操作放到一个单独的线程里,这样我就可以输出....来表示脚本还在运行。

我遇到的问题是,如果文件无法复制,就会抛出一个异常。如果在主线程中运行,这没问题;但是下面的代码在这里就不行:

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

在线程类里面,我尝试重新抛出这个异常,但没有成功。我看到这里有人问过类似的问题,但他们似乎都在做一些比我想做的更具体的事情(我也不太理解他们提供的解决方案)。我还看到有人提到使用sys.exc_info(),但我不知道该在哪里或者怎么使用它。

编辑:下面是线程类的代码:

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

22 个回答

62

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模块来使用类似的功能。

116

这个问题有很多复杂得让人摸不着头脑的回答。我是不是想得太简单了?因为对我来说,这样的处理方式似乎已经足够了。

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, timeout=None):
        super(PropagatingThread, self).join(timeout)
        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 at this')

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
150

问题在于,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()

撰写回答