前言
最近学习Pyqt5,研究QThead线程对象,因网上这方面资料较少,钻研过后,将感悟理解记录如下。
声明:感悟理解建立在分析其他大佬的博客的基础上,喝水不忘挖井人,大佬们的博客如下:
https://huaweicloud.csdn.net/638071cedacf622b8df8844e.html
https://blog.csdn.net/tcy23456/article/details/107904530
https://blog.csdn.net/jeekmary/article/details/88739092
https://www.cnblogs.com/mosewumo/p/12486228.html
1、QThead线程基本使用
线程使用十分简单,我们只需要编写一个类,继承QThead类即可,需要注意以下两点:
1、在自定义线程类的初始化
__init__
方法中,必须调用父类的__init__
方法,否则会报错。
2、重写父类的run()
方法,在此执行业务逻辑。
3、声明自定义线程类的对象之后,调用对象的start()
方法,就会开启线程,进入线程的run()
方法,执行完run()
方法之后,线程结束。
示例如下:
import sys
import timefrom PyQt5.QtCore import *
from PyQt5.QtWidgets import *class MyThread(QThread):def __init__(self):super(MyThread, self).__init__()def run(self):for i in range(10):print(i)time.sleep(1)class MyWindow(QWidget):def __init__(self):super().__init__()self.init_ui()def init_ui(self):self.setWindowTitle("PyQt5多线程学习")self.resize(400, 300)# 开启线程按钮btn_start = QPushButton(self)btn_start.setText("开始")btn_start.clicked.connect(self.start)# 开启线程def start(self):self.my_thread = MyThread()self.my_thread.start()if __name__ == '__main__':app = QApplication(sys.argv)w = MyWindow()w.show()sys.exit(app.exec_())
自定义了线程类MyThread()
,声明对象my_thread
,调用start()
方法,开启线程,会去执行线程中的run()
方法中的内容,每隔一秒输出一个数字。
2、线程暂停与恢复
介绍一下如何实现QThead线程类的恢复与暂停。
我们通过QWaitCondition()
与QMutex()
这两个类来实现这两个功能。
1、QMutex()的作用是给线程上锁与解锁,在线程暂停前先给线程上锁,防止数据状态发生改变,线程被唤醒之后则解锁。线程上锁方法为:QMutex().lock()
,线程解锁方法为:QMutex().unlock()
。
我们只需在线程类中创建QMutex()对象,调用相应的方法即可对线程实现上锁与解锁。
2、QWaitCondition()的作用是暂停线程与恢复线程,暂停线程的方法为:QWaitCondition().wait(QMutex())
,我们需要把上了锁的QMutex()对象当成参数传进去。线程恢复的方法为:QWaitCondition().wakeAll
,可以恢复被wait()过的线程。QWaitCondition()用于多线程同步,一个线程自己调用QWaitCondition.wait()阻塞等待,直到另外一个线程调用QWaitCondition.wake()唤醒该线程,该线程才继续往下执行。
我们给出一个进度条的例子来熟悉这几个函数:
from PyQt5.QtCore import QThread, QWaitCondition, QMutex, pyqtSignal, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QProgressBar, QApplicationclass Thread(QThread):valueChange = pyqtSignal(int)def __init__(self, *args, **kwargs):super(Thread, self).__init__(*args, **kwargs)self._isPause = False # 是否暂停self._value = 0self.cond = QWaitCondition()self.mutex = QMutex()# 暂停(挂起)def pause(self):# 状态改为暂停self._isPause = True# 恢复def resume(self):# 状态改为恢复self._isPause = False# 调用QWaitCondition.wake()唤醒暂停的线程self.cond.wakeAll()def run(self):# 开启线程,一直循环重复下去,监听状态while 1:# 给线程上锁,防止线程挂起的时候,线程中的数据状态发生改变self.mutex.lock()# 如果是暂停状态,则阻塞等待if self._isPause:self.cond.wait(self.mutex)# 进度条不能超过100if self._value > 100:self._value = 0# 每隔0.1秒,进度条+1self._value += 1# 发送发信号,改变进度条的数据self.valueChange.emit(self._value)self.msleep(100)# 线程恢复,解锁self.mutex.unlock()class Window(QWidget):def __init__(self, *args, **kwargs):super(Window, self).__init__(*args, **kwargs)layout = QVBoxLayout(self)self.progressBar = QProgressBar(self)layout.addWidget(self.progressBar)layout.addWidget(QPushButton('休眠', self, clicked=self.doWait))layout.addWidget(QPushButton('唤醒', self, clicked=self.doWake))self.t = Thread(self)# 信号可以连接自定义的函数,也可以连接默认的函数self.t.valueChange.connect(self.progressBar.setValue)self.t.start()# 暂停def doWait(self):self.t.pause()# 恢复def doWake(self):self.t.resume()if __name__ == '__main__':import sys# import cgitbQApplication.setAttribute(Qt.AA_EnableHighDpiScaling)QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)# cgitb.enable(format='text')app = QApplication(sys.argv)w = Window()w.show()sys.exit(app.exec_())
程序中我已经添加注释,不难看懂。
程序运行截图如下:
在此我提几点需要注意的地方:
1、为何run方法中要用while 1
来进行无限循环?
2、为何要添加一个_isPause
来标识线程是否暂停?
3、为何每次循环都要加锁与解锁各一次?
能够回答如上三个问题,说明君以析其中之妙!!!善哉善哉!!!
3、线程退出
与QT类似,我相信如果上面的看懂了,看下面这篇博客也是轻松:
https://blog.csdn.net/qq_44365088/article/details/119087454
怕有些同志静不下心来看不进去,我就顺便在这里说明一下最简单的一种:
1、直接让线程对象调用terminate()
方法来结束线程。终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。请在terminate()之后使用QThread().wait()
警告:此函数是危险的,不鼓励使用。线程可以在其代码路径中的任何点终止。线程可以在修改数据时终止。线程没有机会自己清理,解锁任何持有的互斥锁等。简而言之,只有在绝对必要时才使用这个函数。案例如下,接上文创建线程的代码,只是加一个结束线程的按钮:
import sys
import timefrom PyQt5.QtCore import *
from PyQt5.QtWidgets import *class MyThread(QThread):def __init__(self):super(MyThread, self).__init__()def run(self):for i in range(10):print(i)time.sleep(1)class MyWindow(QWidget):def __init__(self):super().__init__()self.init_ui()def init_ui(self):self.setWindowTitle("PyQt5多线程学习")self.resize(400, 300)# 开启线程按钮btn_start = QPushButton(self)btn_start.setText("开始")btn_start.clicked.connect(self.start)# 结束线程按钮btn_end = QPushButton(self)btn_end.setText("结束")btn_end.move(100, 0)btn_end.clicked.connect(self.end)# 结束线程def end(self):self.my_thread.terminate()# wait函数是个阻塞的接口,意思是线程必须真的退出了,才会执行wait之后的语句,否则将会一直阻塞在这里,如果在界面上使用,需要保证线程中代码的合理性。self.my_thread.wait()print('结束线程')# 开启线程def start(self):self.my_thread = MyThread()self.my_thread.start()if __name__ == '__main__':app = QApplication(sys.argv)w = MyWindow()w.show()sys.exit(app.exec_())
总结
在此再次感谢各位大佬的博客!!
彩蛋
我在此对如上三个问题进行解答:
1、为何run方法中要用while 1来进行无限循环?因为进度条是每隔0.1秒就会改变一次,每次循环都会+1,这是其一。
通过while 1 来实现监听_isPause的状态,这是其二。
2、为何要添加一个_isPause来标识线程是否暂停?线程只能在自己内部调用wait方法将自己暂停,所以需要通过一个标识来判断此时是否应该将自己暂停,并且不断对标识进行监听。
若暂停与唤醒都能在其他线程实现,则不需要标识。
3、为何每次循环都要加锁与解锁各一次?因为无法确定此次循环,是否需要暂停,所以每次循环都加锁与解锁各一次,更加安全。
你可能会说,在:if self._isPause:self.cond.wait(self.mutex)
中不就是需要暂停吗,在这个if中加锁不就行了吗?非也!若在此加锁,那么在那里解锁呢?每次循环都解锁一次吗?没有加锁的循环,
如果解锁的话便会报错。所以每次循环都加锁与解锁一次是最理想的。