《老鸟python 系列》视频上线了,全网稀缺资源,涵盖python人工智能教程,爬虫教程,web教程,数据分析教程以及界面库和服务器教程,以及各个方向的主流实用项目,手把手带你从零开始进阶高手之路!点击 链接 查看详情




工作者线程

阅读:293368537    分享到

消息循环 一节中,我们知道消息的投递和处理是在同一线程中,如果消息处理行为是阻塞或耗时的,这样界面就会停顿下来。我们也给大家介绍了两种解决方案:一种方法是单线程的,如果处理的是阻塞的(IO 密集型),我们可以使用非阻塞 API 替换掉阻塞的 API,如果是耗时的(计算密集型),我们可以用多个消息投递分解耗时的行为; 另一种方法是多线程的,无论消息处理行为是阻塞的还是耗时的,我们都开启一个子线程来处理这种阻塞或耗时的行为。

这种开启一个子线程来处理阻塞或耗时行为的做法,我们叫做工作者线程。对于我们的界面程序来说,消息循环(app._exec())一直运行在主线程中,界面库要求所有的 UI 操作都需要在消息循环所在的线程中完成,所以,对界面程序来说,UI 线程也叫主线程。至于为什么界面库的消息循环必须在主线程中完成,这个是由界面库的特性决定的,同学们如果想深入的了解,可以自行查找相关资料,在此,我就不再鳌述。

我们通常需要 UI 线程和工作者线程进行通信,这样,UI 线程才能使用工作者线程处理的结果,本节课我们就来学习工作者线程和 UI 线程通信问题。

工作者线程和 UI 线程通信详解

为了让大家彻底理解工作者线程和 UI 线程的知识,我们从长计议,首先,我们举例一个耗时的操作,假如我们不开启工作者线程,我们来看看界面程序会是什么样子。

import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel

class OneExample(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.lbl = QLabel('等待结果中...', self)
        self.btn = QPushButton('开始', self)

        self.lbl.move(100, 100)
        self.btn.move(100, 150)

        self.btn.clicked.connect(self.startCount)

        self.setGeometry(600, 200, 800, 500)
        self.setWindowTitle('阻塞耗时例子1')
        self.show()

    def startCount(self):
        time.sleep(10)  # 模拟处理行为需要 10 秒钟
        self.lbl.setText("任务完成")

app = QApplication(sys.argv)
ex = OneExample()
app.exec_()

上面我们用 time.sleep(10) 来模拟消息处理阻塞或耗时 10 秒钟,在这 10 秒钟期间,我们的界面处于卡死状态,在此时,我拖动了一下窗口,相当于向消息队列中投递一个 mousemove 消息,但是这个消息需要等 10 秒之后才能处理,所以界面一直未动,在我连续拖动的情况下界面竟然出现了未响应提醒。

我们采用多线程的方式,把这个阻塞或耗时的操作放到工作者线程中完成,如下例子。

import sys
import time
import threading
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel

class TwoExample(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.lbl = QLabel('等待结果中...', self)
        self.btn = QPushButton('开始', self)

        self.lbl.move(100, 100)
        self.btn.move(100, 150)

        self.btn.clicked.connect(self.startCompute)

        self.setGeometry(600, 200, 800, 500)
        self.setWindowTitle('阻塞耗时例子2')
        self.show()

    def startCompute(self):
        t = threading.Thread(target=self.func)
        t.start()

    def func(self):  # 工作者线程启动的函数
        time.sleep(10)
        self.lbl.setText("任务完成")

app = QApplication(sys.argv)
ex = TwoExample()
app.exec_()

运行上面的程序后,我们发现在工作者线程做任务的 10 秒钟期间,我们可以继续给消息队列投递消息并处理,比如我们拖动窗口,窗口就平滑的移动了起来。

但是,上面的例子有一个严重的 bug,大家还记不记得,在本节一开始,我们刚刚说过,对 UI 的操作必须在主线程中,我们的代码 self.lbl.setText("任务完成"),对 label 控件的操作是在子线程中的,由于我们只是在 label 上显示了文字,程序看似运行的良好(不排除有潜在风险)。如果我们在工作者线程中给 label 上显示图片(PyQt4 会报异常,PyQt5 运作暂时良好,不排除有潜在风险),或者在工作者线程中创建资源(比如调用弹窗,绘制控件,创建控件等等)的操作,程序就会崩溃(PyQt4 和 PyQt5 都会崩溃)。下面的例子,我们就在工作线程中弹出一个对话框。

import sys
import time
import threading
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QMessageBox

class ThreeExample(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.lbl = QLabel('等待结果中...', self)
        self.btn = QPushButton('开始', self)

        self.lbl.move(100, 100)
        self.btn.move(100, 150)

        self.btn.clicked.connect(self.startCompute)

        self.setGeometry(600, 200, 800, 500)
        self.setWindowTitle('阻塞耗时例子3')
        self.show()

    def startCompute(self):
        t = threading.Thread(target=self.func)
        t.start()

    def func(self):  # 工作者线程启动的函数
        time.sleep(10)
        QMessageBox.information(None, "工作者线程", "老鸟python")  # 非 UI 线程中创建资源

app = QApplication(sys.argv)
ex = ThreeExample()
app.exec_()

我们发现程序崩溃了,那工作者线程和 UI 线程正确的编程姿势应该是什么呢,大家只需要记住一点,工作者线程只需要把处理的结果发给主线程,我们对 UI 的操作都需要在主线程中进行。正确的写法如下。

import sys
import time
import threading
from PyQt5.Qt import pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QMessageBox

class FourExample(QWidget):
    sinOut = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.sinOut.connect(self.outResult)
        self.initUI()

    def initUI(self):
        self.lbl = QLabel('等待结果中...', self)
        self.btn = QPushButton('开始', self)

        self.lbl.move(100, 100)
        self.btn.move(100, 150)

        self.btn.clicked.connect(self.startCompute)

        self.setGeometry(600, 200, 800, 500)
        self.setWindowTitle('阻塞耗时例子4')
        self.show()

    def outResult(self, rst):  # 在主线程(UI 线程)中操作 UI
        self.lbl.setText(rst)
        QMessageBox.information(None, "工作者线程", "老鸟python")

    def startCompute(self):
        t = threading.Thread(target=self.func)
        t.start()

    def func(self):  # 工作者线程做运算
        time.sleep(10)
        self.sinOut.emit("任务完成")  # 把结果投递到消息队列

app = QApplication(sys.argv)
ex = FourExample()
app.exec_()

在工作者线程调用的函数 func 中,我们做了阻塞或耗时的运算(用 time.sleep(10) 来模拟),运算完成后,我们使用了 self.sinOut.emit("任务完成")往消息队列中投递一个消息,而我们的消息循环一直在主线程中运行,此时,消息循环函数从消息队列里取出该消息,然后调用了消息处理函数 outResult(self, rst),其中 rst 参数是工作者线程投递消息时附带的参数,该参数是工作者线程想给我们的任何东西。

我们在主线程运行的函数 outResult(self, rst) 中,做了 UI 操作(self.lbl.setText(rst)QMessageBox.information(None, "工作者线程", "老鸟python")),我们发现程序运行良好。

使用 QThread

在上例的程序中,在同一个类 FourExample中,既有主线程调用的函数 outResult(self, rst),又有工作者线程调用的函数 func(self),这样使得我们感觉很乱,我们采用面向对象的方式来重新编写工作者线程和 UI 线程通信的案例,下面我们写一个线程类来继承 Qt 的线程类 QThread。

import sys
import time
from PyQt5.Qt import pyqtSignal, QThread
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QMessageBox

class FiveExample(QWidget):
    sinOut = pyqtSignal(str)  # 创建一个消息对象

    def __init__(self):
        super().__init__()
        self.workthd = Workthread(self.sinOut)  # 创建一个线程对象
        self.sinOut.connect(self.outResult)     # 给消息对象关联槽函数
        self.initUI()

    def initUI(self):
        self.lbl = QLabel('等待结果中...', self)
        self.btn = QPushButton('开始', self)

        self.lbl.move(100, 100)
        self.btn.move(100, 150)

        self.btn.clicked.connect(self.startCompute)

        self.setGeometry(600, 200, 800, 500)
        self.setWindowTitle('阻塞耗时例子5')
        self.show()

    def outResult(self, rst):  # 在主线程(UI 线程)中操作 UI
        self.lbl.setText(rst)
        QMessageBox.information(None, "工作者线程", "老鸟python")

    def startCompute(self):
        self.workthd.start()   # 启动工作者线程

class Workthread(QThread):
    def __init__(self, sinOut):
        super(Workthread, self).__init__()
        self.sinOut = sinOut

    def run(self):  # 工作者线程做任务
        time.sleep(10)
        self.sinOut.emit("任务完成")

app = QApplication(sys.argv)
ex = FiveExample()
app.exec_()

本节重要知识点

会写工作者线程和 UI 线程通信。

牢记要在主线程中对 UI 进行操作。

作业

在 label 中每隔 3-5 秒显示一张图片,保持界面的流畅(用多线程实现)。


如果以上内容对您有帮助,请老板用微信扫一下赞赏码,赞赏后加微信号 birdpython 领取免费视频。


登录后评论

user_image
Jasin-Yip
2020年10月29日 23:39 回复

我打算写一写


user_image
lookas2001
2020年10月24日 19:16 回复

大佬请收下我的膝盖,解决了困惑我好久的问题!


user_image
王祖贤
2020年6月9日 12:09 回复

很好,正解了我的疑惑


user_image
Ra酱
2020年6月7日 01:53 回复

赞赞赞,大佬对界面编程的核心探究的很深啊!


user_image
写作之欲正在高涨
2019年12月18日 16:37 回复

给力,终于弄明白工作者线程了~~


user_image
王政
2019年6月3日 10:53 回复

学习了,谢谢你


user_image
Dustin
2018年9月23日 18:00 回复

老铁精辟