pyqt5 Qthread的子线程异常处理

本文探讨了如何在PyQt5中使用多线程实现图像数据处理,避免UI线程崩溃。作者分享了如何通过在子线程中定义异常标志并连接异常信号,以确保主线程在子线程异常时能够捕获并处理。同时,还介绍了如何结合进度条改进异常时的用户体验。
摘要由CSDN通过智能技术生成

项目场景:

使用pyqt5开发一个带有UI界面的图像数据处理程序

问题描述:

根据业务和UI界面分离的原则,为了使大量数据处理过程不影响UI主进程,于是使用了多线程的方式,利用pyqt5中的QThread类来创建数据处理的子线程,然后将数据传回UI主线程进行展示。然而在子线程处理数据过程中,由于文件读取等操作可能会产生异常,进而引起主UI线程的崩溃。为了提高程序的鲁棒性和健壮性,使用try...except处理异常,然而子线程中产生的异常无法在主线程中捕获到

# 数据处理的线程
class WorkThread(QThread):
    finish = pyqtSignal(np.ndarray) # 定义一个信号,类型是numpy数组类型的图片

    def __init__(self,path):
        super(WorkThread, self).__init__()
        self.path = path

    def __del__(self):
        self.wait()

    def run(self):
        image = cv2.imread(self.path)
        time.sleep(5) # 模拟处理数据过程
        self.finish.emit(image) # 处理好的图片数据发送出去

# 主UI线程
class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__init__()
        self.label = QLabel(self)
        self.bt = QPushButton("处理",self)
        self.bt.clicked.connect(self.pocessImage)
        vBox = QVBoxLayout()
        vBox.addWidget(self.bt)
        vBox.addWidget(self.label)
        self.setLayout(vBox)

    def pocessImage(self):
        self.bt.setEnabled(False)
        path = "test.jpg"
        workThread = WorkThread(path)
        workThread.start() #启动线程
        workThread.finish.connect(self.showImage)

    def showImage(self,image):
        self.bt.setEnabled(True)
        # QImage通过numpy类型转化QImage类型时,注意后面两个参数写全
        # openCV读取的通道顺序为BGR
        self.label.setPixmap(QPixmap(QImage(image,image.shape[1], image.shape[0],image.shape[1]*3, QImage.Format_BGR888)))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWidget()
    window.show()
    sys.exit(app.exec())
	

原因分析:

使用start()方法启动子线程时,解释器会为子线程开辟独立的栈空间,于是主线程就无法获取子线程栈的信息。当子线程异常中止时,会在子线程中捕获处理而不会将此异常抛出给主线程

解决方案:

在子线程中定义一个异常标志,如果线程异常退出,将该标志位设置为1,正常退出为0

class WorkThread(QThread):
    finish = pyqtSignal(np.ndarray) # 定义一个信号,类型是numpy数组类型的图片

    def __init__(self,path):
        super(WorkThread, self).__init__()
        self.path = path
        self.exitcode = 0  # 如果线程异常退出,将该标志位设置为1,正常退出为0
        self.exception = None


    def run(self):
        try:
            image = cv2.imread(self.path)
            time.sleep(2) # 模拟处理数据过程
            self.finish.emit(image) # 处理好的图片数据发送出去
        except Exception as e:
            self.exitcode = 1
            self.exception = e

class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__init__()
        self.label = QLabel(self)
        self.bt = QPushButton("处理",self)
        self.bt.clicked.connect(self.pocessImage)
        vBox = QVBoxLayout()
        vBox.addWidget(self.bt)
        vBox.addWidget(self.label)
        self.setLayout(vBox)

    def pocessImage(self):
        try:
            self.bt.setEnabled(False)
            path = "test.jp" # 错误路径,人为产生一个异常
            self.workThread = WorkThread(path)
            self.workThread.start() #启动线程
            self.workThread.finish.connect(self.showImage)

            if self.workThread.wait() and self.workThread.exitcode == 1: #self.workThread.wait()不可缺少,需要等主线程完成后再进行子线程活动
                raise self.workThread.exception

        except Exception as e:
            self.bt.setEnabled(True)
            QMessageBox.information(self, "提示:", '操作失败!')

    def showImage(self,image):
        self.bt.setEnabled(True)
        # QImage通过numpy类型转化QImage类型时,注意后面两个参数写全
        # openCV读取的通道顺序为BGR
        self.label.setPixmap(QPixmap(QImage(image,image.shape[1], image.shape[0],image.shape[1]*3, QImage.Format_BGR888)))

问题基本上解决! 但是关于Pyqt多线程相关的知识还是没有完全掌握和理解,后面继续学习!

更新

在项目中有一个小功能,是用进度条来显示后台任务完成的进度。遇到的问题是,如果后台任务没有发生异常,进度条可以很好的显示进度,但是发生异常后,进度条窗口会进入“假死”状态,使用上面的方法无法解决这种问题,该进度条只能手动关闭才能显示后面的异常信息,否则进度条会一直显示。

新的解决思路

在子线程中创建一个异常信号,然后在子线程中捕获异常,如果子线程任务出现了异常就emit这个异常信号,在主UI线程中connect这个异常信号,如果接收成功了就说明子线程有异常,就可以对其进行相应的操作(直接关闭进度条)。

部分代码:

# 进度条类
class ProcessDialog(QDialog):
    def __init__(self):
        super(ProcessDialog,self).__init__()
        self.resize(300,100)
        vBox = QVBoxLayout()
        label = QLabel("处理中...",self)
        self.pbar = QProgressBar()
        self.pbar.setMaximum(100)
        self.timer = QBasicTimer()
        self.step = 0
        vBox.addWidget(label)
        vBox.addWidget(self.pbar)
        self.setLayout(vBox)
        self.setWindowTitle('提示')
        self.timer.start(10, self)
        # 新建的窗口始终位于当前屏幕的最前面
        self.setWindowFlags(Qt.WindowStaysOnTopHint)
        # 阻塞父类窗口不能点击
        self.setWindowModality(Qt.ApplicationModal)
        self.setWindowFlags(Qt.WindowCloseButtonHint)


    def timerEvent(self, e): # 监测,如果step大于等于100则关闭进度条
        if self.step >= 100:
            self.quit()
            return

        # if self.step == 0:
        #     QTimer.singleShot(10000, self.quit)

    def quit(self):
        QApplication.processEvents() #会使窗口变换流畅
        self.step = 0
        self.pbar.setValue(self.step)
        self.timer.stop()
        self.close()

# 子线程类
class Mythread(Qthread):
	finish = pyqtSignal(np.ndarray,int)  #处理完后待发送的信息信号,int用来控制进度条的进度
    excepted = pyqtSignal(str) #发送一个异常信号
    
    def __init__(self):
       super(Mythread, self).__init__()
    
    def run(self):
	    try:
	    	# 进行耗时操作
	    	for i in range(100):
	    		self.finish.emit(image,i+1) # 处理完成后发送
	    except Exception as e:
	    	self.excepted.emit(str(e)) # 将异常发送给UI进程
    	
 
# 窗口UI类      
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        # 部分代码省略
        self.myThread = MyThread()
        self.myThread.finish.connect(self.fun)
        self.myThread.excepted.connect(self.threadException)
        self.processDiaog = ProcessDialog()
        if not self.processDiaog.exec_(): #显示进度条,如果手动关闭则终止子线程
           self.myThread.terminate()
    	
    def fun(image,step): #正常处理函数
    	self.processDiaog.step = step
        self.processDiaog.pbar.setValue(step)
        # 对image数据进行展示
        # 略
        
    def threadException(self,message):
    	self.processDiaog.quit()
    	print(message) #打印子线程错误日志
    	  	
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值