目录
前言
本文主要讲解PyQt使用多线程模块QThread解决PyQt界面程序执行耗时操作时,程序卡顿出现的无响应以及界面输出无法实时显示的问题。有时候,我们写的程序界面出现未响应,是因为把需要长时间运行的代码放在了主线程,阻塞了事件循环。
QtCore.QThread是一个管理线程的类,当我们使用其构造函数的时候,便新建了一个线程。这里要强调,QThread是一个线程管理器,不要把业务逻辑放在这个类里面,Qt的作者已经多次批评继承QThread类来实现业务逻辑的做法。然而我在网上看了很多资料或者博客,很大一部分都是继承QThread实现实现业务逻辑...当然也有很多科学使用的教程,不过相对来说较少。我今天就把我的科学使用PyQt多线程,心里路程分享出来,供大家参考。
PyQt线程科学用法
非科学用法样例
上面介绍了非科学用法,是直接继承QThread在里面写业务。那么网上很多教程也就是这样的....
class Thread(QThread):
def __init__(self):
super(Thread,self).__init__()
def run(self):
#业务逻辑代码
#创建一个新的线程
thread = Thread()
thread.start()
科学用法
Qt的作者已经多次批评继承QThread类来实现业务逻辑的做法,那么怎么使用才是科学使用PyQt多线程呢?答案如下:
自己定义一个线程类,这个类要继承QObject,在里面实现线程的相关业务逻辑,同时在主界面里实例化这个线程类,然后用moveToThread方法移动到QThread管理。
# fixme PyQt线程科学用法
self.thread = QThread()
#实例外线程对象 workThread是自己写的线程类,后面会贴出来
self.work_thread = workThread()
# 把实例化的线程用moveToThread移到QThread管理
self.work_thread.moveToThread(self.thread)
线程类
首先写好自己的线程类,实现业务功能 ,因为我这里是由于图像处理耗时,直接把原来的代码贴过来,稍加修改的。要注意的是线程之间的通信,以及是否会有资源竞争等情况。 QtCore.Signal和QtCore.pyqtSignal是一样的,我这里这样写是开源的labelme是这样的,我就采用原样写法。都是信号定义的方式。
class workThread(QObject):
#图像处理完成信号
to_show_img_signal = QtCore.Signal(QtGui.QImage)
def __init__(self):
super(workThread, self).__init__()
def work(self):
global imageData
global mask_list
"""
省略图像
处理相关代码
"""
#处理完成发送信号
self.to_show_img_signal.emit(qimage)
线程通信
线程通信的方式:全局变量,消息传递(PyQt信号槽机制)
我这里采用的是全局变量,在Mianwindow类和workThread类之外定义了两个全局变量,在两个类使用到改变量都需要加global,才能实现全局效果。
大家可能会疑问,我为什么要使用全局变量,作为线程通信的方式,为什么不用 信号槽?我最开始也是想用信号槽,也是这么做的,但是会报错。因为我这里使用线程的同时需要主界面发送图像数据给子线程,子线程处理完毕后,发送给主界面显示。也就是线程双向通信。
信号槽机制:子线程向主页面发送信号以及数据确实很方便。但是主界面发送数据给子线程就会出问题。使用信号槽程序会出现如下错误,获取不到信号的connect方法。
'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect
上面的报错,我以前也遇到过,也有解决方案,但是这次问题和这个报错不沾边的....
信号的正确定义和使用以及上面报错解决方案:https://memory-qianxiao.blog.csdn.net/article/details/105754667
所以我就猜想信号槽是子线程向主界面发送信号使用的,如果要主界面发送数据给子线程,需要其他方法实现。
线程类在主界面实例化与使用
这里贴出来的代码是我代码里面线程使用精简化后的,为了方便大家理解,同时写了注释,比较核心的几行代码就是init里面那几行。这个是科学使用PyQt线程的模板了。尤其注意线程完毕时,需要关闭线程, 线程不会自己关闭的。
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# fixme PyQt线程科学用法
self.thread = QThread()
# 实例化线程类
self.work_thread = workThread()
#moveToThread方法把实例化线程移到Thread管理
self.work_thread.moveToThread(self.thread)
# 线程开始执行之前,从相关线程发射信号
self.thread.started.connect(self.work_thread.work)
#接收子线程信号发来的数据
self.work_thread.to_show_img_signal.connect(self.show_img_in_labelme)
# 线程执行完成关闭线程
self.thread.finished.connect(self.threadStop)
def threadStart(self):
# 开启线程
self.thread.start()
def threadStop(self):
# 退出线程
self.thread.quit()
#接收线程数据的槽函数
def show_img_in_labelme(self, qimage):
self.onNewBrightnessContrast(qimage)
开启线程
我们这里需要特别注意,线程的开启应在主界面里面,当有需要处理耗时操作的时候,就主动开启一个线程,处理耗时任务,处理完毕,检测线程执行完毕,就需要关闭。上面已经定义了开启方法。所以只需要在用到的地方调用函数即可。
def eraser_or_brush(self, coordinate):
#全局变量
global mask_list
global imageData
#业务逻辑代码
"""橡皮擦功能"""
"""笔刷功能(逆向橡皮擦)"""
#主动开启一个线程
self.thread.start()
# 设置撤销按钮是否可用
self.actions.undo.setEnabled(self.isHasMaskImage())
补充(信号的方式实现线程双向通信):
补充时间:20201年1月5日
上面的方式是基于全局变量,实现主线程和子线程的通信,子线程处理完毕向主线程发送信号。现在这里补充另一种写法思路:在主线程一开始(init初始化)就开启一个线程挂在后台,在有需要的的时候,就发送数据和信号,去让线程处理,处理完毕在发送信号给主线程。
线程类
与上面的线程类不同的是这里线程处理方法多了两个参数一个信号。
from qtpy.QtCore import QObject, QThread
from qtpy import QtCore, QtGui
import cv2
import numpy as np
from . import utils
import time
class ImageProcessingThread(QObject):
#处理完毕图像数据信号
to_show_img_signal = QtCore.Signal(QtGui.QImage)
#线程接收参数信号
to_start_image_process_thread_signal = QtCore.Signal(bytes, list)
def __init__(self):
super(ImageProcessingThread, self).__init__()
#接收两个个参数的方法
def work(self, imageData, mask_list):
"""图像处理业务过程"""
#处理完毕发送信号
self.to_show_img_signal.emit(qimage)
线程实例化与开启线程挂在后台
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
# fixme PyQt线程科学用法
self.thread = QThread()
# 初始化线程类传参
self.image_process_thread = ImageProcessingThread()
self.image_process_thread.moveToThread(self.thread)
# 连接槽函数
self.image_process_thread.to_start_image_process_thread_signal.connect(self.image_process_thread.work)
self.image_process_thread.to_show_img_signal.connect(self.show_img_in_labelme)
# 开启线程 一直挂在后台
self.thread.start()
发送信号,线程处理数据
def eraser_or_brush(self, coordinate):
# 业务逻辑代码
"""橡皮擦功能"""
"""笔刷功能(逆向橡皮擦)"""
# 发送信号给线程,让线程开始工作
self.image_process_thread.to_start_image_process_thread_signal.emit(self.imageData, self.mask_list)
# 设置撤销按钮是否可用
self.actions.undo.setEnabled(self.isHasMaskImage())
结语
希望我这篇文章对你有所帮助,希望三连,不胜感激~
多线程,多进程这一块其实还是挺难的,虽然代码很短,但是要具体实现某个复杂一点的功能,要控制好真的不的不容易~要花大量时间去采坑,去看别人的参考资料~而现在网上资料太多了,很多博主又是直接把转载或者把别人文章原样不动发表,让我们要花更多的时间去看找资料,真的很难受~
我的这篇文章主要是把我的心得体会与精简化的代码贴出来,供大家学习,相当于一个模版。大家少了采坑的过程,可能会对多线程理解还不够,不能理解我的一些做法,不过我会把我觉得有用参考文章给大家贴到后面。供大家去参考,理解。
参考文章
点击字体即可进入文章