PyQt之科学使用线程处理耗时任务以及线程通信方法 约定不等于承诺〃 2023-01-01 15:54 109阅读 0赞 **目录** 前言 PyQt线程科学用法 非科学用法样例 科学用法 线程类 线程通信 线程类在主界面实例化与使用 开启线程 补充(信号的方式实现线程双向通信): 线程类 线程实例化与开启线程挂在后台 发送信号,线程处理数据 结语 参考文章 -------------------- # 前言 # 本文主要讲解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][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()) # 结语 # 希望我这篇文章对你有所帮助,希望三连,不胜感激~ 多线程,多进程这一块其实还是挺难的,虽然代码很短,但是要具体实现某个复杂一点的功能,要控制好真的不的不容易~要花大量时间去采坑,去看别人的参考资料~而现在网上资料太多了,很多博主又是直接把转载或者把别人文章原样不动发表,让我们要花更多的时间去看找资料,真的很难受~ 我的这篇文章主要是把我的心得体会与精简化的代码贴出来,供大家学习,相当于一个模版。大家少了采坑的过程,可能会对多线程理解还不够,不能理解我的一些做法,不过我会把我觉得有用参考文章给大家贴到后面。供大家去参考,理解。 # 参考文章 # 点击字体即可进入文章 * [使用PyQt线程的正确姿势][PyQt] * [pyqt5 的多线程(QThread)遇到的坑(一)][pyqt5 _QThread] * [PyQt5开发学习(三)--使用moveToThread异步刷新UI][PyQt5_--_moveToThread_UI] * [pyqt多线程moveToThread的使用][pyqt_moveToThread] [https_memory-qianxiao.blog.csdn.net_article_details_105754667]: https://memory-qianxiao.blog.csdn.net/article/details/105754667 [PyQt]: https://blog.csdn.net/qq_39607437/article/details/79213717?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-2&spm=1001.2101.3001.4242 [pyqt5 _QThread]: https://blog.csdn.net/wuwei_201/article/details/104720386 [PyQt5_--_moveToThread_UI]: https://blog.csdn.net/imzhujun/article/details/87215942 [pyqt_moveToThread]: https://blog.csdn.net/renee1996/article/details/85098268
还没有评论,来说两句吧...