实战PyQt5: 128-使用多线程进行并行处理

多线程是实现并行处理的重要手段,在GUI编程中,经常需要将耗费任务分离,用单独的线程来处理,避免对主线程造成影响(最常见的影响就是会造成主界面无法响应的假死现象)。在Qt中,最常用的多线程一般是通过继承QThread类,重载其函数run()来实现。

QThread简介

QThread类提供了一种独立于平台的方式来管理线程。一个QThread对象管理程序中的一个控制线程。在run()中执行。默认情况下,run()通过调用exec()启动事件循环,并在线程内部运行Qt事件循环。

可以使用QObject.moveToThread()的方式将工作对象移动到线程中执行。

class Worker (QObject):
   resultReady = pyqtSignal(str)
  
   def __init__(self, parent=None):
       super(WorkerThread, self)._init__(parent)
  
   def doWork(self, parameter):
       result=''
       #这里执行耗时的阻塞操作
       self.resultReady.emit(result)
 
class Controller(QObject):
   operate=pyqtSignal(str)
   def __init__(self, parent=None):
       super(WorkerThread, self)._init__(parent)
       worker = Worker()
       self.workerThread =QThread()
       worker.moveToThread(self.workerThread)
       workerThread.finished.connect(worker.deleteLater)
       self.operate.connect(worker.doWork)
       worker.resultReady.connect(self.handleResults)
       self.workerThread.start()
  
   def __del__(self):
       self.workerThread.quit()
       self.workerThread.wait()
 
   def handleResults(sel, result):
       #在这里处理工作结束后返回的信息

然后, Worker中的doWork()代码将在单独的线程中执行。在这里我们可以将Worker的doWork()槽函数连接到来自任何线程的任何信号,因为借助Qt中的队列连接的机制,可以安全地跨线程连接信号和槽。

另外一种使用线程的方式就是继承QThread并重新实现其run() 函数。

class WorkerThread(QThread):
   resultReady = pyqtSignal(str)
  
   def __init__(self, parent=None):
       super(WorkerThread, self).__init__(parent)
 
   def run(self):
       result=''
       #这里执行耗时的阻塞操作
       self.resultReady.emit(result)
 
   def __del__(self):
       self.quit()
       self.wait()
 
class MyObject(QObject):
   ....
   def startWorkInAThread(self):
       workerThread = WorkerThread(self)
       workerThread.resultReady.connect(self.handleResults)
       workerThread.finished.connect(workerThread.deleteLater)
       workerThread.start()
 
   def handleResults(sel, result):
       #在这里处理工作结束后返回的信息

QThread.Priority线程优先级枚举量:

  • QThread.IdlePriority (0): 仅在没有其他线程在运行时调度。
  • QThread.LowestPriority (1): 比LowPriority安排的时间少。
  • QThread.LowPriority (2): 比NormalPriority安排的时间少。
  • QThread.NormalPriority (3): 操作系统的默认优先级。
  • QThread.HighPriority (4): 比NormalPriority安排的时间更多。
  • QThread.HighestPriority (5): 比“高优先级”安排的时间更频繁。
  • QThread.TimeCriticalPriority (6): 尽可能频繁地安排。
  • QThread.InheritPriority (7): 使用与创建线程相同的优先级。这是默认值。

QThread常用函数:

  • usleep(usecs): 静态函数,强制当前线程休眠usecs微秒。
  • msleep(msecs): 静态函数,强制当前线程休眠msecs毫秒。
  • sleep(secs): 静态函数,强制当前线程休眠secs秒。
  • run(self): 在调用start之后,线程将调用此函数。QThread的默认实现只是调用exec()。
  • setPriority(self, priority: 'QThread.Priority'): 设置线程优先级。
  • priority(self): 返回线程的优先级设置。
  • start(self, priority: 'QThread.Priority'): 通过调用run()开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,则此函数不执行任何操作。
  • wait(self): 阻塞线程,直到满足以下任一条件:1.与该QThread对象关联的线程已完成执行(即,从run()返回时)。如果线程完成,此函数将返回true。如果线程尚未启动,它也会返回true。2.执行期限已到达。如果到了最后期限,此函数将返回false。
  • quit(self): 告诉线程的事件循环以返回码0(成功)退出。等效于调用QThread.exit(0)。如果线程没有事件循环,则此函数不执行任何操作。
  • exit(self, returnCode: int): 告诉线程的事件循环以returnCode退出。
  • terminate(self): 终止线程的执行。根据操作系统的调度策略,线程可能会立即终止,也可能不会立即终止。可以在终止()之后使用QThread.wait()。当线程终止时,所有等待线程完成的线程都将被唤醒。警告:此功能很危险,不建议使用。线程可以在其代码路径中的任何位置终止。修改数据时可以终止线程。线程自身无法清除、解锁任何保持的互斥锁等。总之,仅在绝对必要时才使用此功能。
  • isRunnig(self): 线程正在运行返回True,否则返回False。
  • isFinished(self): 线程已结束返回True,否则返回False。

QThread常用信号:

  • finish(self): 线程在执行完成之前发出该信号。发出此信号时,事件循环已经停止运行。除了延迟的删除事件外,线程中将不再处理其他事件。该信号可以连接到QObject.deleteLater(),以释放该线程中的对象。

注意:如果关联的线程使用terminate终止线程,则不确定从哪个线程发出该信号。

注意:这是一个私有信号。它可以用于信号连接,但不能由用户发射。

  • started(self): 在调用run()函数之前,该信号在开始执行时从关联线程发出。

注意:这是一个私有信号。它可以用于信号连接,但不能由用户发射。

测试

测试代码在pyqt5-examples的线程样例代码基础上,演示了使用多线程绘制Mandelbrot分形图像。 完整代码如下:

import sys
from PyQt5.QtCore import (Qt, pyqtSignal, QPoint, QSize,
                          QMutex, QMutexLocker, QWaitCondition, QThread)
from PyQt5.QtGui import QColor, QImage, QPainter, QPixmap, qRgb
from PyQt5.QtWidgets import QApplication, QWidget
 
#一些控制变量
DefaultCenterX = -0.647011
DefaultCenterY = -0.0395159
DefaultScale = 0.00403897
 
ZoomInFactor = 0.8
ZoomOutFactor = 1 / ZoomInFactor
ScrollStep = 20
 
#图像渲染线程
class RenderThread(QThread):
    ColormapSize = 512
    
    #图像渲染完成后,发射这个信号
    renderedImage = pyqtSignal(QImage, float)
    
    def __init__(self, parent = None):
        super(RenderThread, self).__init__(parent)
        
        self.mutex = QMutex()
        self.condition = QWaitCondition()
        self.centerX = 0.0
        self.centerY = 0.0
        self.scaleFactor = 0.0
        self.resultSize = QSize()
        self.colormap = []
        
        self.restart = False
        self.abort = False
        
        for i in range(RenderThread.ColormapSize):
            self.colormap.append(self.rgbFromWaveLength(380.0 + (i * 400.0 / RenderThread.ColormapSize)))
            
    def __del__(self):
        self.mutex.lock()
        self.abort = True
        self.condition.wakeOne()
        self.mutex.unlock()
        self.wait()
        
    def render(self, centerX, centerY, scaleFactor, resultSize):
        locker = QMutexLocker(self.mutex)
        
        self.centerX = centerX
        self.centerY = centerY
        self.scaleFactor = scaleFactor
        self.resultSize = resultSize
        
        if not self.isRunning():
            self.start(QThread.LowestPriority)
        else:
            self.restart = True
            self.condition.wakeOne()
            
    def run(self):
        while True:
            self.mutex.lock()
            resultSize = self.resultSize
            scaleFactor = self.scaleFactor
            centerX = self.centerX
            centerY = self.centerY
            self.mutex.unlock()
            
            halfWidth = resultSize.width() // 2
            halfHeight = resultSize.height() // 2
            image = QImage(resultSize, QImage.Format_RGB32)
            
            NumPasses = 8
            curpass = 0
            
            # 渲染Mandelbort分形几何图像
            while curpass < NumPasses:
                MaxIterations = (1 << (2 * curpass + 6)) + 32
                Limit = 4
                allBlack = True
                
                for y in range(-halfHeight, halfHeight):
                    if self.restart:
                        break
                    if self.abort:
                        return
                    
                    ay = 1j * (centerY + (y * scaleFactor))
                    
                    for x in range(-halfWidth, halfWidth):
                        c0 = centerX + (x * scaleFactor) + ay
                        c = c0
                        numIterations = 0
                        
                        while numIterations < MaxIterations:
                            numIterations += 1
                            c = c*c + c0
                            if abs(c) >= Limit:
                                break
                            numIterations += 1
                            c = c*c + c0
                            if abs(c) >= Limit:
                                break
                            numIterations += 1
                            c = c*c + c0
                            if abs(c) >= Limit:
                                break
                            numIterations += 1
                            c = c*c + c0
                            if abs(c) >= Limit:
                                break
                        
                        if numIterations < MaxIterations:
                            image.setPixel(x + halfWidth, y + halfHeight,
                                           self.colormap[numIterations % RenderThread.ColormapSize])
                            allBlack = False
                        else:
                            image.setPixel(x + halfWidth, y + halfHeight, qRgb(0, 0, 0))
                
                if allBlack and curpass == 0:
                    curpass = 4
                else:
                    if not self.restart:
                        self.renderedImage.emit(image, scaleFactor)
                    curpass += 1
            
            self.mutex.lock()
            if not self.restart:
                self.condition.wait(self.mutex)
            self.restart = False
            self.mutex.unlock()
    
    #根据波长计算对应的填充颜色
    def rgbFromWaveLength(self, wave):
        r = 0.0
        g = 0.0
        b = 0.0
 
        if wave >= 380.0 and wave <= 440.0:
            r = -1.0 * (wave - 440.0) / (440.0 - 380.0)
            b = 1.0
        elif wave >= 440.0 and wave <= 490.0:
            g = (wave - 440.0) / (490.0 - 440.0)
            b = 1.0
        elif wave >= 490.0 and wave <= 510.0:
            g = 1.0
            b = -1.0 * (wave - 510.0) / (510.0 - 490.0)
        elif wave >= 510.0 and wave <= 580.0:
            r = (wave - 510.0) / (580.0 - 510.0)
            g = 1.0
        elif wave >= 580.0 and wave <= 645.0:
            r = 1.0
            g = -1.0 * (wave - 645.0) / (645.0 - 580.0)
        elif wave >= 645.0 and wave <= 780.0:
            r = 1.0
 
        s = 1.0
        if wave > 700.0:
            s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0)
        elif wave < 420.0:
            s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0)
 
        r = pow(r * s, 0.8)
        g = pow(g * s, 0.8)
        b = pow(b * s, 0.8)
 
        return qRgb(r*255, g*255, b*255)        
        
 
class DemoMandelbrot(QWidget):
    def __init__(self, parent=None):
        super(DemoMandelbrot, self).__init__(parent)   
        
         # 设置窗口标题
        self.setWindowTitle('实战 Qt for Python: QTread演示')      
        # 设置窗口大小
        self.resize(480, 360)
      
        self.thread = RenderThread()
        self.pixmap = QPixmap()
        self.pixmapOffset = QPoint()
        self.lastDragPos = QPoint()
 
        self.centerX = DefaultCenterX
        self.centerY = DefaultCenterY
        self.pixmapScale = DefaultScale
        self.curScale = DefaultScale
 
        self.thread.renderedImage.connect(self.updatePixmap)
        self.setCursor(Qt.CrossCursor)
        
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.black)
 
        if self.pixmap.isNull():
            painter.setPen(Qt.white)
            painter.drawText(self.rect(), Qt.AlignCenter,
                    "正在渲染初始化图像,请耐心等待......")
            return
 
        if self.curScale == self.pixmapScale:
            painter.drawPixmap(self.pixmapOffset, self.pixmap)
        else:
            scaleFactor = self.pixmapScale / self.curScale
            newWidth = int(self.pixmap.width() * scaleFactor)
            newHeight = int(self.pixmap.height() * scaleFactor)
            newX = self.pixmapOffset.x() + (self.pixmap.width() - newWidth) / 2
            newY = self.pixmapOffset.y() + (self.pixmap.height() - newHeight) / 2
 
            painter.save()
            painter.translate(newX, newY)
            painter.scale(scaleFactor, scaleFactor)
            #exposed, _ = painter.matrix().inverted()
            exposed = painter.transform()
            exposed = exposed.mapRect(self.rect()).adjusted(-1, -1, 1, 1)
            painter.drawPixmap(exposed, self.pixmap, exposed)
            painter.restore()
 
        text = "使用鼠标滚轮或者 '+' 和 '-' 键进行缩放. 按下鼠标左键并保持,进行拖动"
        metrics = painter.fontMetrics()
        textWidth = metrics.width(text)
 
        painter.setPen(Qt.NoPen)
        painter.setBrush(QColor(0, 0, 0, 127))
        painter.drawRect((self.width() - textWidth) / 2 - 5, 0, textWidth + 10,
                metrics.lineSpacing() + 5)
        painter.setPen(Qt.white)
        painter.drawText((self.width() - textWidth) / 2,
                metrics.leading() + metrics.ascent(), text)
        
    def resizeEvent(self, event):
        self.thread.render(self.centerX, self.centerY, self.curScale,
                self.size())
 
    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Plus:
            self.zoom(ZoomInFactor)
        elif event.key() == Qt.Key_Minus:
            self.zoom(ZoomOutFactor)
        elif event.key() == Qt.Key_Left:
            self.scroll(-ScrollStep, 0)
        elif event.key() == Qt.Key_Right:
            self.scroll(+ScrollStep, 0)
        elif event.key() == Qt.Key_Down:
            self.scroll(0, -ScrollStep)
        elif event.key() == Qt.Key_Up:
            self.scroll(0, +ScrollStep)
        else:
            super(DemoMandelbrot, self).keyPressEvent(event)
 
    def wheelEvent(self, event):
        numDegrees = event.angleDelta().y() / 8
        numSteps = numDegrees / 15.0
        self.zoom(pow(ZoomInFactor, numSteps))
 
    def mousePressEvent(self, event):
        if event.buttons() == Qt.LeftButton:
            self.lastDragPos = QPoint(event.pos())
 
    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.pixmapOffset += event.pos() - self.lastDragPos
            self.lastDragPos = QPoint(event.pos())
            self.update()
 
    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.pixmapOffset += event.pos() - self.lastDragPos
            self.lastDragPos = QPoint()
 
            deltaX = (self.width() - self.pixmap.width()) / 2 - self.pixmapOffset.x()
            deltaY = (self.height() - self.pixmap.height()) / 2 - self.pixmapOffset.y()
            self.scroll(deltaX, deltaY)
 
    def updatePixmap(self, image, scaleFactor):
        if not self.lastDragPos.isNull():
            return
 
        self.pixmap = QPixmap.fromImage(image)
        self.pixmapOffset = QPoint()
        self.lastDragPosition = QPoint()
        self.pixmapScale = scaleFactor
        self.update()
 
    def zoom(self, zoomFactor):
        self.curScale *= zoomFactor
        self.update()
        self.thread.render(self.centerX, self.centerY, self.curScale,
                self.size())
 
    def scroll(self, deltaX, deltaY):
        self.centerX += deltaX * self.curScale
        self.centerY += deltaY * self.curScale
        self.update()
        self.thread.render(self.centerX, self.centerY, self.curScale,
                self.size())
    
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = DemoMandelbrot()
    window.show()
    sys.exit(app.exec())   

运行结果如下图:

实战PyQt5: 128-使用多线程进行并行处理

测试QThread

本文知识点

  • 为什么要在程序中使用多线程。
  • 使用QThread的两种方法。
  • 使用多线程绘制耗时的几何图像。

前一篇: 实战PyQt5: 127-处理XML文档

  • 5
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值