第 18 章
多线程
本章共 5 个小节 · PySide6 Basic Tutorial
本章要点:
- QThread类的常见用法;
- 线程池(QThreadPool);
- 互斥锁(QMutex);
- 条件等待(QWaitCondition)。
18.1
单线程与多线程的比较
应用程序进程在初始化时会创建一个默认线程(也叫主线程)用于处理GUI(图形用户界面)逻辑。
如果在主线程上执行需要长时间运行的代码,会导致窗口及其所包含的组件无法接收用户输入(鼠标单击、拖动,或键盘输入),最终导致窗口无响应,甚至整个程序会崩溃,如图18-1所示。
如果把耗费时间的代码放到另一个线程上运行,那么主线程仍可以继续与用户交互,窗口组件就不会无响应了,如图18-2所示。
因此,若应用程序要处理比较耗时的任务(如下载大文件、进行大量计算),最好把它放在新线程上运行,以保证图形界面具有良好的用户体验。
18.2
QThread 类
QThread是编写多线程代码的基础类,它提供了许多底层API,灵活性高。一个QThread实例只负责管理一个线程。QThread类实例化后,新线程不会马上执行。要启动新线程请调用start方法。线程启动后会发出started信号,线程执行完毕后会发出finished信号。
QThread类支持事件循环,run方法的默认实现会调用exec方法启动事件循环。事件循环启动后会一直等待和处理事件,直到调用quit或exit方法才会结束。事件循环结束标志着线程退出。
QThread类有以下两种用法。
- 定义新类,从 QThread 类派生并重写 run 方法。在 run 方法中加入需要的代码逻辑。当 start方法被调用后,run方法也会被调用,从而执行自定义代码。
- 定义从QObject类派生的新类,将自定义的逻辑代码封装到该类中,然后用moveToThread方法将对象移动到指定线程(QThread实例)上,通过信号/槽机制激活自定义代码。
18.2.1 示例:随机变换颜色
本示例将实现每隔3秒更换一次颜色的功能,颜色是随机生成的,其代码将在新的线程上运行。本示例采用直接从QThread类派生的方式实现多线程,具体步骤如下。
- 定义 CustThread类,基类是 QThread。
class CustThread(QThread):
# 信号
colorGen = Signal(QColor)
# 执行任务的代码
def run(self):
# 进入循环
while self.isInterruptionRequested() == False:
# 生成3个随机整数
r, g, b = randint(0, 255), randint(0, 255), randint(0, 255)
# 创建QColor 实例
c = QColor.fromRgb(r, g, b)
# 发出信号
self.colorGen.emit(c)
# 暂停一会儿
QThread.sleep(3)
GUI线程与其他线程之间的代码不能直接访问,但可以通过信号和槽(Signals and Slots)相互调用。
当生成新的颜色后,CustThread对象就会发送colorGen信号。GUI线程可以连接该信号,并接收新的颜色值。
CustThread类重写了run方法,实现随机生成r、g、b 3个整数,并用这 3个数据值产生新的 QColor实例。
- 定义DemoWindow类,派生自QWidget类,它是示例程序的主窗口。
class DemoWindow(QWidget):
- 实现__init__方法,初始化窗口布局。
def __init__(self, parent: QWidget = None):
super().__init__(parent)
# 窗口标题
self.setWindowTitle("随机变换颜色")
# 窗口大小
self.resize(300, 320)
# 窗口布局
rootlayout = QGridLayout()
self.setLayout(rootlayout)
# 网格的第一行放一个自定义Qwidget
self._wg = QWidget(self)
self._wg.setAutoFillBackground(True)
rootlayout.addWidget(self._wg, 0, 0, 1, 3)
# 网格的第二行是两个按钮
self._btnStart = QPushButton("开始", self)
self._btnStop = QPushButton("停止", self)
rootlayout.addWidget(self._btnStart, 1, 0)
rootlayout.addWidget(self._btnStop, 1, 2)
# 调整行高比例
rootlayout.setRowStretch(0, 1)
# 调整列宽比例
rootlayout.setColumnStretch(0, 2)
rootlayout.setColumnStretch(1, 1)
rootlayout.setColumnStretch(2, 2)
窗口使用了网格布局。第一行是一个QWidget对象(_wg字段),跨三列布局;第二行放了两个按钮,用于控制新线程的启动和停止。
- 创建 CustThread 实例。
self._th = CustThread(self)
- 两个按钮的 clicked信号需要连接到CustThread 对象的方法成员上,以达到启动和停止线程任务的目的。
# 线程启动由start方法触发
self._btnStart.clicked.connect(self._th.start)
# requestInterruption 方法可以停止任务
self._btnStop.clicked.connect(self._th.requestInterruption)
requestInterruption方法可以请求中断线程,并可以通过isInterruptionRequested方法返回该状态(True表示已调用了requestInterruption 方法)。上文中 run 方法的实现代码中,while循环的条件是未调用requestInterruption 方法(isInterruptionRequested方法返回 False)。
terminate方法也可以停止线程(如果此功能被禁用,可调用 setTerminationEnabled方法并向参数传递True 来开启),但最好不要使用此方法来停止线程。因为 terminate 方法不会留给线程足够的时间去完成清理工作(如保存数据),很容易造成数据损坏或不可预知的错误。
- 连接线程对象的started和finished信号,实现在线程状态改变后修改按钮的可用状态。
self._th.started.connect(self.changeBtnEnabled)
self._th.finished.connect(self.changeBtnEnabled)
以下是 changeBtnEnabled 方法的实现代码。
def changeBtnEnabled(self):
running = self._th.isRunning()
self._btnStart.setEnabled(not running)
self._btnStop.setEnabled(running)
isRunning方法返回 True表示线程正在运行;如果返回False表示线程已经结束。当线程运行后,禁用“开始”按钮并启用“停止”按钮;当线程停止后,禁用“停止”按钮并启用“开始”按钮。
- 连接 CustThread 对象的 colorGen 信号,在获得新颜色后修改 wg 的背景颜色(通过调色板实现)。
self._th.colorGen.connect(self.setColor)
@Slot(QColor)
def setColor(self, color: QColor):
# 获取调色板
p = self._wg.palette()
# 修改颜色
p.setColor(QPalette.ColorRole.Window, color)
# 重新设置调色板
self._wg.setPalette(p)
setColor 方法上添加了Slot 类作为装饰器,标注它是一个槽(Slot)对象,可以与 colorGen信号连接。
运行示例程序,单击“开始”按钮,QWidget组件的背景色会不断变化,如图18-3所示。在颜色变换过程中用户仍可以进行拖动窗口或调整窗口大小等操作,这表明GUI线程没有被阻断。
18.2.2 示例:使用moveToThread 方法
moveToThread是QObject类公开的方法成员,允许将Qt对象从当前线程转移到另一个线程上。该对象的子级对象也会一同转移到新线程上。调用moveToThread方法的对象不能有父级对象(不能设置parent),否则无法将对象转移到新线程上。
本示例将定义一个名为MyWorker的类(以QObject为基类)用于执行耗时操作。实例化后可以将它转移到新的 QThread 对象上执行。MyWorker类的实现代码如下:
class MyWorker(QObject):
# 用于报告进度的信号
reportProgress = Signal(int)
# 用于报告任务完成的信号
reportComplete = Signal()
@Slot(int, int)
def RunTask(self, min: int, max: int):
# 表示当前进度的变量
currVal = min
print("正在处理……")
# 进入循环
while currVal < max:
# 暂停片刻
QThread.msleep(60)
# 增加进度
currVal += 1
# 报告进度
self.reportProgress.emit(currVal)
print("处理完毕")
# 报告任务已完成
self.reportComplete.emit()
MyWorker类定义了两个信号:reportProgress 用于报告处理进度,reportComplete 用于报告任务已完成。GUI线程上的代码可以连接这两个信号,以获取任务的实时状态。
RunTask 方法应用了Slot 装饰器,可以被 GUI线程上的信号连接。当GUI线程发出信号时,RunTask方法就会调用。GUI线程不能直接调用RunTask方法,那样做会使RunTask的代码在GUI线程上执行,导致GUI线程被阻塞,程序窗口不能及时响应用户输入。因此,GUI线程必须通过信号来调用RunTask方法。
定义 TestWindow类,派生自 QWidget,作为程序的窗口类。该窗口上分别创建了进度条组件(QProgressBar)和按钮组件(QPushButton)。代码如下:
self._pb = QProgressBar(self)
#设置最大/最小进度值
self._pb.setMaximum(100)
self._pb.setMinimum(0)
......
#按钮
self._btn = QPushButton("多线程处理", self)
......
TestWindow 类定义 runWorker 信号,用于连接到 MyWorker 对象的 RunTask 方法。
class TestWindow(QWidget):
# 信号
runWorker = Signal(int, int)
......
def __init__(self):
super().__init__()
......
# 实例化线程类
self._th = QThread()
# 实例化线程任务对象
self._worker = MyWorker()
# 移动到新线程上
self._worker.moveToThread(self._th)
# 将runWorker 信号与MyWorker 实例的RunTask方法连接
self.runWorker.connect(self._worker.RunTask)
注意,不能向MyWorker类的构造函数传递父级对象引用(如当前窗口类的实例self),否则moveToThread方法将无效。
TestWindow 类需要两个方法成员与 MyWorker 对象的 reportProgress、reportComplete 信号建立连接,代码如下:
# 连接报告任务已完成的信号
self._worker.reportComplete.connect(self.onCompleteReport)
# 连接报告进度的信号
self._worker.reportProgress.connect(self.onProgressReported)
......
@Slot(int)
def onProgressReported(self, p: int):
# 更新进度条的当前值
self._pb.setValue(p)
@Slot()
def onCompleteReport(self):
self._btn.setEnabled(True)
self._pb.setValue(0)
QMessageBox.information(self,"提示","任务已完成")
TestWindow与MyWorker 对象之间的信号连接关系如图18-4所示。
为了实现在关闭窗口时能马上终止线程,TestWindow类可以重写closeEvent方法,具体代码如下:
def closeEvent(self, event: QCloseEvent):
# 在窗口关闭前强制终止线程
self._th.terminate()
# 等待线程退出
self._th.wait()
# 调用基类成员
super().closeEvent(event)
此处调用terminate方法强制终止线程,wait方法用于等待线程退出。
运行示例程序,单击“多线程处理”按钮,进度条开始更新,直至完成。在此过程中,用户仍可以与窗口交互,如图18-5所示。
18.3
QThreadPool
在应用程序中频繁创建和销毁QThread实例,会消耗许多不必要的时间,使性能降低;而且创建过多的QThread实例也会增加内存的占用。QThreadPool类(线程池)可重复使用已创建的线程资源,减少创建QThread实例的次数,大幅降低内存占用,提升程序效率。这一过程是由QThreadPool自动管理的,应用程序只要将包含任务代码的 QRunnable 对象传递给 QThreadPool 对象的 start 方法即可。
18.3.1 QRunnable
QRunnable用于定义需要在线程池上运行的代码。使用时需要从 QRunnable类派生,并且重写 run方法,实现自定义逻辑。
每个应用程序都有一个全局的 QThreadPool 对象,可通过 globalInstance静态方法获取其引用。调用QThreadPool.start方法并向参数传递 QRunnable 实例即可启动任务。默认行为下,QRunnable 对象执行完毕后,QThreadPool会自动将其删除。可以调用QRunnable.setAutoDelete方法来禁用自动删除功能。
18.3.2 示例:向 QThreadPool投放 3 个任务
本示例将演示向线程池(QThreadPool)放入3个做加法运算的任务。具体实现过程如下。
- 定义MyTask类。该类属于多继承,基类是QRunnable和QObject类,代码如下:
class MyTask(QRunnable, QObject):
# 信号:报告计算结果
setResult = Signal(int)
def __init__(self, parent: QObject = None):
# 调用 QObject 类的构造函数
QObject.__init__(self, parent)
# 调用 QRunnable 类的构造函数
QRunnable.__init__(self)
# 最终数值
self._final = 0
# 禁止自动删除实例
self.setAutoDelete(False)
def run(self):
print(f"正在计算,当前线程ID:{current_thread().ident}")
# 表示结果的变量
result = 0
# 开始累加
current = 1
while current <= self._final:
result = result + current
current += 1
# 暂停一下
QThread.msleep(2)
# 计算完毕,设置结果
self.setResult.emit(result)
# 设置参与累加运算的最终值
@Slot(int)
def setFinalNumber(self, n: int):
self._final = n
MyTask类是任务类,用于实现在后台线程进行加法运算。从 QObject 类派生是因为 MyTask 需要用到信号和槽与GUI线程通信。
setResult信号可以将计算结果发送给GUI线程,并显示在用户界面上。setFinalNumber方法用于设置一个整数,该整数是参与加法运算的最大值。例如,数值设置为6,程序就会计算1+2+3+4+5+6的结果。
从QRunnable类派生必须重写 run方法,进行从1开始的累加运算,一直加到_final所指定的值。
计算结束后,发出 setResult信号,通知GUI线程显示结果。
在 MyTask 的 __init__ 方法中,一定要明确调用 QRunnable 和 QObject 类的 __init__ 方法(调用基类的构造函数),否则将无法访问基类的成员。setAutoDelete(False) 表示禁止 QThreadPool 类接管 MyTask 的生命周期。如果 setAutoDelete 方法设定为 True(默认值),那么,当 run 方法执行完毕后就会删除 MyTask 实例。
- 从 QWidget 派生出一个名为 CustWidget 的自定义组件。该组件内包含可以输入数值的 QSpinBox组件,可显示计算结果的QLabel组件,以及可以启动后台线程进行加法运算的按钮组件。
class CustWidget(QWidget):
# 信号:用于设置累加运算的最终值
setFinnalValue = Signal(int)
def __init__(self, parent: QWidget = None):
super().__init__(parent)
# 布局
layout = QGridLayout()
self.setLayout(layout)
# 标签
layout.addWidget(QLabel("最大值: ", self), 0, 0)
# 数值输入组件
self._spin = QSpinBox(self)
self._spin.setRange(1, 10000)
self._spin.setValue(5)
layout.addWidget(self._spin, 0, 1)
# 按钮
self._btnAct = QPushButton("计算", self)
layout.addWidget(self._btnAct, 0, 2)
# 显示结果的标签
self._lbRes = QLabel(self)
layout.addWidget(self._lbRes, 1, 0, 1, 3)
layout.setColumnStretch(1, 1)
# 任务实例
self._task = MyTask()
# setFinnalValue 信号连接到 _task 的 setFinalNumber 方法
self.setFinnalValue.connect(self._task.setFinalNumber)
# 连接 setResult,获取计算结果
self._task.setResult.connect(self.onGetResult)
# 连接按钮的 clicked 信号
self._btnAct.clicked.connect(self.onClick)
@Slot(int)
def onGetResult(self, res: int):
self._lbRes.setText(f'计算结果:{res}')
# 恢复按钮
self._btnAct.setEnabled(True)
def onClick(self):
# 清空上次显示的计算结果
self._lbRes.clear()
# 设置最终值
self.setFinnalValue.emit(self._spin.value())
# 将 _task 放入线程池中运行
QThreadPool.globalInstance().start(self._task)
# 禁用按钮
self._btnAct.setEnabled(False)
setFinnalValue信号连接MyTask对象的setFinalNumber方法,可以传递参与运算的最大数值;onGetResult方法使用了Slot装饰器,该方法将与MyTask对象的setResult信号连接,接收来自其他线程传来的计算结果。
- 定义DemoWindow类,它是应用程序的主窗口。
class DemoWindow(QWidget):
def __init__(self):
super().__init__()
# 布局
rootLayout = QVBoxLayout()
self.setLayout(rootLayout)
# 创建3个自定义组件
wg1 = CustWidget(self)
wg2 = CustWidget(self)
wg3 = CustWidget(self)
rootLayout.addWidget(wg1)
rootLayout.addWidget(wg2)
rootLayout.addWidget(wg3)
def closeEvent(self, ev: QCloseEvent):
# 等待所有任务完成
thPool= QThreadPool.globalInstance()
if thPool.activeThreadCount() > 0:
ev.ignore()
return
super().closeEvent(ev)
窗口类重写了基类的closeEvent方法,如果线程池上还有正在活动的线程,就阻止关闭窗口(ev.ignore()将取消close 事件)。activeThreadCount方法可以获得线程池中仍在运行的线程数。
- 运行示例程序,输入3个适当的整数值(建议数值不要太大),如图18-6所示。
- 先单击第一个“计算”按钮,然后等待任务完成,再单击第二个“计算”按钮。同理,第三个按钮也要等第二个任务完成再单击。此时控制台输出的内容如下:
正在计算,当前线程ID:6932
正在计算,当前线程ID:6932
正在计算,当前线程ID:6932
可以看到,3个任务都是在同一个线程上运行的。由于这3个任务并不是同时进行的,因此QThreadPool对象只创建了一个新线程来处理。
- 重新运行示例程序,输入稍大一点的数值(主要为了延长计算时间,但数值不要过大),然后单击第一个“计算”按钮启动任务。不等第一个任务完成马上单击第二个“计算”按钮,启动第二个任务。同样地,不需要等待第二个任务完成,直接启动第三个任务。控制台打印的内容如下:
正在计算,当前线程ID:2928
正在计算,当前线程ID:2924
正在计算,当前线程ID:6632
这一次3个任务所在线程不同,表明QThreadPool对象创建了3个新线程来运行任务。由于3个任务运行时间较长,各自占用一个线程,QThreadPool无法重新利用现有线程,所以必须为每个任务创建新线程来运行代码。
18.4
互斥锁
当多个线程同时访问某个对象时,极容易造成不一致的状态。举一个经典案例:多线程递减整数值。
某整数变量的初始值为100,doWork函数负责递减整数的值,直到整数等于0。基本代码如下:
# 全局数值
number = 100
# 递减数值
def doWork():
global number # 声明number为全局变量
while number > 0:
# 稍稍暂停一下
QThread.msleep(4)
# number变量递减
number = number - 1
print(f"当前数值:{number}")
注意,在上述代码中,循环条件number>0与number=number-1之间暂停了4毫秒(由QThread.msleep 方法实现)。在暂停的时间内会发生不可控的情况——其他线程执行了number=number-1。假设A、B、C 3个线程同时运行doWork函数,如果线程A在验证 while循环的条件时number变量的值为 1,那么 1>0成立,进入循环,可是当即将对number变量做递减运算时,C线程抢先一步把number减掉1,使其变成了0;B线程又抢先一步把number的值减去1,变成了-1。而A线程并不知道number已经小于0,于是又减去1,直接导致number变成了-2。显然这一结果是不符合预期的。不妨用图18-7来模拟这个过程。
要解决这个问题,就必须限制在同一时刻只允许一个线程访问(或修改)数据。当A线程获取到数据时将其锁定,B、C线程只能等待;A线程处理完毕后解锁数据,此时B线程获得数据访问权并将数据锁定,C线程只能等待解锁。
Qt应用程序可以使用QMutex类为线程上锁。QMutex类实例化后,在需要访问数据的线程上调用lock方法即可锁定资源,访问过后调用unlock方法解锁资源,使正在等待的线程可以访问数据。
前文提到的doWork函数可以修改为:
# 全局数值
number = 100
# 实例化互斥对象
mutex = QMutex()
# 递减数值
def doWork():
global number # 声明number为全局变量
# 锁定数据
mutex.lock()
# 处理数据……
# 解除锁定
mutex.unlock()
18.4.1 示例:将 QMutex用于多线程运算
本示例实现的功能为:用户输入一个整数(例如5),然后对该整数进行以下三次数学运算(假设N是输入的整数)。
1、N=N*2+15
2、N=(N- 5)*20
3、N = 100* (N /2- 10)
这3个计算过程分别写到3个函数中。
def _fun1(self):
num = self._number
QThread.msleep(3)
num = num * 2 + 15
self._number = num
def _fun2(self):
num = self._number
QThread.msleep(5)
num = (num -5) * 20
self._number = num
def _fun3(self):
num = self._number
QThread.msleep(7)
num = 100 * (num / 2 - 10)
self._number = num
上述3个函数中,在读取_number字段的值后暂停了若干毫秒。在这暂停过程中,number字段的值可能会被其他线程意外修改,导致错误的计算结果。
将3个函数依次放入线程池(QThreadPool)中运行。
self._threadPool.start(self._fun1)
self._threadPool.start(self._fun2)
self._threadPool.start(self._fun3)
# 等待计算完成
self._threadPool.waitForDone()
# 显示结果
self._lbRes.setText(f"{self._number}")
waitForDone方法会等待线程池中所有任务完成后才会返回,此处是等待3个函数执行结束。
假设输入整数10,预期的正确结果是29000,但程序给出的结果是-500,如图18-8所示。
接下来为程序代码加上互斥锁。
# 获取全局的QThreadPool对象引用
self._threadPool = QThreadPool.globalInstance()
# 实例化互斥锁
self._mutex = QMutex()
修改3个函数的代码,在执行计算前锁定数据,计算完成后解锁,代码如下:
def _fun1(self):
self._mutex.lock()
......
self._mutex.unlock()
def _fun2(self):
self._mutex.lock()
......
self._mutex.unlock()
def _fun3(self):
self._mutex.lock()
......
self._mutex.unlock()
上锁之后,在同一时刻只有一个线程可以读写number字段的值。再次运行示例程序就能得到正确的结果了,如图18-9所示。
18.4.2 示例:使用 QMutexLocker 类
QMutex 对象每次访问资源时都要先调用lock方法锁定数据,处理完毕后调用 unLock方法解锁数据。如果在代码量非常大的程序中,可能会频繁调用lock和unLock方法,一不小心就会出现在调用lock方法后忘记调用unLock方法的情况。
QMutexLocker类将简化上锁与解锁操作。当QMutexLocker 实例化时自动上锁;当 QMutexLocker实例被销毁时自动解锁。因此,QMutexLocker类型的变量一般声明在函数内部,在程序进入函数时给数据上锁;执行完函数体后,在退出函数时QMutexLocker变量的生命周期结束,从而解锁数据。
本示例以出售火车票为例,演示QMutexLocker类的使用。示例的完整代码如下:
# 火车票总数
total = 1000
# 售票函数
def sale():
# total 是全局变量,要先声明一下
# 否则会被视为函数本地变量,导致全局变量被覆盖
global total
while total > 0:
QThread.msleep(3)
# 售出一张票
total = total - 1
QThread.msleep(3)
# 打印剩余票数
print(f"剩余 {total} 张火车票")
if __name__=="__main__":
app = QCoreApplication()
# 5个线程同时售票
for n in range(5):
QThreadPool.globalInstance().start(sale)
# 等待售票结束
QThreadPool.globalInstance().waitForDone()
上述代码中,sale函数在修改全局变量 total时是没有加互斥锁的,因此在运行后会出现剩余火车票数量为负数的情况。控制台输出如下:
剩余 2 张火车票
剩余 -1 张火车票
剩余 -1 张火车票
剩余 -3 张火车票
剩余 -3 张火车票
剩余 -3 张火车票
随后为sale函数加上互斥锁,代码的修改如下:
# 火车票总数
total = 1000
# 实例化互斥锁对象
mutex = QMutex()
# 售票函数
def sale():
# total 是全局变量,要先声明一下
# 否则会被视为函数本地变量,导致全局变量被覆盖
global total
# 上锁
with QMutexLocker(mutex):
while total > 0:
QThread.msleep(3)
# 售出一张票
total = total - 1
QThread.msleep(3)
# 打印剩余票数
print(f"剩余 {total} 张火车票")
# 即将退出函数,自动解锁
QMutexLocker 类实现了__enter__和__exit__方法,因此支持在 with 语句中使用。
再次运行程序,就能得到正确的输出了。
....
剩余 7 张火车票
剩余 6 张火车票
剩余 5 张火车票
剩余 4 张火车票
剩余 3 张火车票
剩余 2 张火车票
剩余 1 张火车票
剩余 0 张火车票
18.5
QWaitCondition
QWaitCondition类是线程间一种通信信号,允许一个线程向其他线程发出信号。例如,A线程可以调用wait方法等待信号;B线程调用wakeOne或wakeAll方法发出信号,唤醒A线程;A线程收到信号后会从wait方法返回并继续执行,如图18-10所示。
wait方法在调用时需要接收一个QMutex对象,并且在调用前QMutex对象必须上锁。wait方法内部会先让QMutex对象解锁,然后等待信号。wait方法在返回之前会让QMutex对象重新上锁。也就是说,QMutex 对象在 wait方法调用前后都会处于锁定状态。
唤醒等待线程的方法有两个:wakeOne方法随机唤醒一个线程;wakeAll方法将唤醒所有正在等待的线程。
18.5.1 示例:多阶段任务
本示例将启动3个线程任务。其中,第二个任务必须等第一个任务完成后才能继续;第三个任务必须等待第二个任务完成后才能继续。因此,需要创建两个QWaitCondition实例。
# 全局互斥锁
mutex = QMutex()
# 代表第一阶段
step1 = QWaitCondition()
# 代表第二阶段
step2 = QWaitCondition()
由于第三阶段后没有等待的线程,所以只需要两个QWaitCondition对象即可。3个线程分别运行以下3个函数。
def work1():
print("第一阶段开始")
QThread.sleep(2)
print("第一阶段完成")
# 唤醒正在等待的线程
mutex.lock()
step1.wakeOne()
mutex.unlock()
def work2():
# 等待第一阶段完成
mutex.lock()
step1.wait(mutex)
mutex.unlock()
print("第二阶段开始")
QThread.sleep(1)
print("第二阶段完成")
# 唤醒正在等待的线程
mutex.lock()
step2.wakeOne()
mutex.unlock()
def work3():
# 等待第二阶段完成
mutex.lock()
step2.wait(mutex)
mutex.unlock()
print("第三阶段开始")
QThread.sleep(2)
print("第三阶段完成")
第一阶段(work1)执行完成后,调用 step1 的 wakeOne 方法通知 work2。
第二阶段(work2)在执行任务前调用了step1.wait方法,此时 mutex对象会临时解除锁定,并等待 step1.wakeOne的调用。当等到 step1 的信号后继续执行,完成后调用 step2.wakeOne 方法,通知其他线程第二阶段完成。
第三阶段(work3)先要等待 step2.wait收到信号才返回,然后完成后面的代码。
在主线程中,通过QThreadPool类启动上述3个线程。
# 启动3个线程
pool = QThreadPool.globalInstance()
pool.start(work1)
pool.start(work2)
pool.start(work3)
# 等待所有线程结束
pool.waitForDone()
print("所有任务均已完成")
pool.waitForDone方法将处于等待状态,直到3个线程都结束。上述代码用到了start方法的另一个重载,不需要实现QRunnable类,直接传递要运行的函数引用即可。该重载的声明如下:
def start(arg__1: Callable, priority: int = ...)
运行示例程序,控制台将输出以下内容。
第一阶段开始
第一阶段完成
第二阶段开始
第二阶段完成
第三阶段开始
第三阶段完成
所有任务均已完成
18.5.2 示例:等待键盘输入
本示例将演示多个线程等待同一个信号的实现方法。3个后台线程在启动后均处于等待状态。主线程会提示用户输入两个整数值。当应用程序获取到输入数值后,调用QWaitCondition对象的wakeAll方法唤醒正在等待的3个线程。随后这3个线程分别对输入的两个整数进行加、减、乘法运算,并向控制台打印计算结果。
示例程序需要定义以下全局变量。
# 全局互斥锁
mutex = QMutex()
# 等待条件
numbersGot = QWaitCondition()
# 用于计算的数值
number1 = 0
number2 = 0
然后定义3个函数,用于进行算术运算。这些函数将运行在线程池中。
def action1():
# 等待输入
mutex.lock() # 上锁
numbersGot.wait(mutex)
# 计算
r = number1 + number2
# 打印结果
print("{0}+{1}={2}".format(number1, number2, r))
mutex.unlock() # 解锁
def action2():
# 等待输入
mutex.lock() # 上锁
numbersGot.wait(mutex)
# 计算
r = number1 - number2
# 打印结果
print("{0}-{1}={2}".format(number1, number2, r))
mutex.unlock() # 解锁
def action3():
# 等待输入
mutex.lock() # 上锁
numbersGot.wait(mutex)
# 计算
r = number1 * number2
# 打印结果
print("{0}*{1}={2}".format(number1, number2, r))
mutex.unlock() # 解锁
将上述3个函数放入线程池中运行。随后读取键盘输入。
# 获取全局线程池
thPool = QThreadPool.globalInstance()
# 启动3个线程
thPool.start(action1)
thPool.start(action2)
thPool.start(action3)
# 等待输入
s1 = input("请输入第一个整数:")
s2 = input("请输入第二个整数:")
# 此变量标志在获取用户输入时是否发生了错误
hasErr = False
try:
number1 = int(s1)
number2 = int(s2)
except:
# 输入的内容可能不是整数值
print("您输入的可能不是整数")
number1 = 0
number2 = 0
hasErr = True # 标记已发生错误
如果成功获取到两个整数值,就向正在等待的3个线程发出信号。
if not hasErr:
# 通知等待的线程
mutex.lock()
numbersGot.wakeAll()
mutex.unlock()
# 等待所有线程结束
thPool.waitForDone()
运行示例程序,依次输入整数10、3,控制台将输出以下结果:
10*3=30
10-3=7
10+3=13
3个线程获取的互斥锁的序列是随机的,因此3个函数的执行顺序每次运行都会不一样。有可能先进行乘法运算,再进行加法运算。