前言
最近学习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 time
from 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, QApplication
class Thread(QThread):
valueChange = pyqtSignal(int)
def __init__(self, *args, **kwargs):
super(Thread, self).__init__(*args, **kwargs)
self._isPause = False # 是否暂停
self._value = 0
self.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)
# 进度条不能超过100
if self._value > 100:
self._value = 0
# 每隔0.1秒,进度条+1
self._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 cgitb
QApplication.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 time
from 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中加锁不就行了吗?非也!若在此加锁,那么在那里解锁呢?每次循环都解锁一次吗?没有加锁的循环,
如果解锁的话便会报错。所以每次循环都加锁与解锁一次是最理想的。