PyQt5:具有多处理功能的单个 QProgressBar 卡住了

Posted

技术标签:

【中文标题】PyQt5:具有多处理功能的单个 QProgressBar 卡住了【英文标题】:PyQt5: single QProgressBar with multiprocessing gets stuck 【发布时间】:2021-11-01 09:23:47 【问题描述】:

我有一个很长的列表要处理,所以我使用多处理来加快处理速度。现在我想在 PyQt5.QtWidgets.QProgressBar 中显示进度。这是代码:

import sys
from PyQt5.QtWidgets import *
import multiprocessing as mp
import threading

targets = list(range(0, 100))


def process(i):
    print("target:", i)
    # do something, for example:
    for c in range(1000):
        for r in range(1000):
            c = c * r + 4


class MainWin(QWidget):

    def __init__(self):
        super(MainWin, self).__init__()
        self.setupUi()

    def setupUi(self):
        self.setFixedSize(500, 90)
        self.layout = QGridLayout()
        self.main_widget = QWidget(self)
        self.progressBar = QProgressBar()
        self.progressBar.setValue(0)

        self.btn = QPushButton('start')

        self.layout.addWidget(self.progressBar, 0, 0, 1, 1)
        self.layout.addWidget(self.btn, 1, 0, 1, 1)
        self.setLayout(self.layout)

        self.btn.clicked.connect(self.run)

    def display(self, args):
        self.progressBar.setValue(self.progressBar.value() + 1)
        print("process bar:", self.progressBar.value())
        # QApplication.processEvents()  # I've tried this function and it has no effect

    def run(self):
        def func(results):
            pool = mp.Pool()
            for t in targets:
                pool.apply_async(process, (t,), callback=self.display)
                results.append(t)
            pool.close()
            pool.join()

        results = []
        t = threading.Thread(target=func, args=(results,))
        t.start()
        # everything is fine without t.join(), but the progress bar always gets stuck when t.join() is called
        # t.join()
        # pass  # do something with the results


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_win = MainWin()
    main_win.show()
    sys.exit(app.exec_())

没有调用“t.join()”一切都很好。但是要获得完整的“结果”,我必须等待线程结束,在这种情况下 processBar 总是卡在 40% 左右。

如何解决这个问题?

【问题讨论】:

【参考方案1】:

PyQt5(和一般的Qt)需要在主线程中不断运行事件循环才能发挥作用:重绘GUI,对用户输入做出反应等。如果你调用t.join(),主线程就会卡在里面run 方法,阻止线程和所有 GUI 更新,包括重绘进度条。为了正常运行,代码应该尽快退出run,所以t.join()不是一个好的解决方案。

其中一种处理方法是使用 Qt 信号。首先,等待它们不会阻塞主循环,因此 GUI 保持响应。其次,它们在主线程中执行,因此从非主线程访问 Qt 小部件没有问题(这通常是个坏主意)。以下是我建议重写代码的方式:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import pyqtSignal, pyqtSlot
import multiprocessing as mp
import threading

targets = list(range(0, 100))


def process(i):
    print("target:", i)
    # do something, for example:
    for c in range(1000):
        for r in range(1000):
            c = c * r + 4


class MainWin(QWidget):

    def __init__(self):
        super(MainWin, self).__init__()
        self.setupUi()
        self.done = False

    def setupUi(self):
        self.setFixedSize(500, 90)
        self.layout = QGridLayout()
        self.main_widget = QWidget(self)
        self.progressBar = QProgressBar(self.main_widget)
        self.progressBar.setValue(0)

        self.btn = QPushButton('start',self.main_widget)

        self.layout.addWidget(self.progressBar, 0, 0, 1, 1)
        self.layout.addWidget(self.btn, 1, 0, 1, 1)
        self.setLayout(self.layout)

        self.btn.clicked.connect(self.run)
        self.single_done.connect(self.display)
        self.all_done.connect(self.process_results)

    single_done = pyqtSignal()
    @pyqtSlot()
    def display(self):
        self.progressBar.setValue(self.progressBar.value() + 1)
        print("process bar:", self.progressBar.value())
    
    all_done = pyqtSignal()
    @pyqtSlot()
    def process_results(self):
        print("Processing results")
        pass  # do something with the results

    def run(self):
        def func(results):
            pool = mp.Pool()
            for t in targets:
                pool.apply_async(process, (t,), callback=lambda *args: self.single_done.emit())
                results.append(t)
            pool.close()
            pool.join()
            self.all_done.emit()

        results = []
        t = threading.Thread(target=func, args=(results,))
        t.start()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_win = MainWin()
    main_win.show()
    sys.exit(app.exec_())

我添加了两个信号:single_done,每次完成单个目标执行时都会发出;all_done,在所有处理完成时发出。在setupUi 的末尾,它们连接到用于更新进度条和处理结果的相应方法。 run 不再停留并立即退出,结果的处理现在在 process_result 方法中完成,该方法在完成时调用。

顺便说一句,使用信号报告中间结果也可以消除您可能已经收到的QObject::setParent: Cannot set parent, new parent is in a different thread 警告。这是因为现在display 是在正确的线程中调用的(因为它是使用信号调用的),而之前它是在线程t 中直接调用的,它不拥有任何小部件。

【讨论】:

太棒了!现在它完美运行了!

以上是关于PyQt5:具有多处理功能的单个 QProgressBar 卡住了的主要内容,如果未能解决你的问题,请参考以下文章

Pyqt5进度条QProgressBar的使用/多线程更新/按钮美化/图片编码/开机自启动

如何在具有值而不是单个变量值的列表上应用 PyQt5 事件? [关闭]

PyQt5 pyqtgraph(添加/删除)曲线在单个图中

具有不同功能的多进程池

如何设计:具有多线程信号的单个阻塞工作者

PyQt5实现邮件合并功能(GUI)