在 Qt 中显示进度条,后台进程计算量很大 [英] Show progressbar in Qt with computationally heavy background process

查看:97
本文介绍了在 Qt 中显示进度条,后台进程计算量很大的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在构建一个应用程序,让用户导出他/她的工作.这是一个计算量很大的过程,持续一分钟左右,在此期间我想显示一个进度条(并使 UI 的其余部分无响应).

I'm building an application that let's the user export his/her work. This is a computationally heavy process, lasting for a minute or so, during which I want to show a progress bar (and make the rest of the UI unresponsive).

我已经尝试了下面的实现,它适用于非计算昂贵的后台进程(例如等待 0.1 秒).但是,对于 CPU 繁重的进程,UI 变得非常滞后和无响应(但并非完全无响应).

I've tried the implementation below, which works fine for a non-computationally expensive background process (e.g. waiting for 0.1 s). However, for a CPU heavy process, the UI becomes very laggy and unresponsive (but not completely unresponsive).

知道如何解决这个问题吗?

Any idea how I can solve this?

import sys
import time

from PySide2 import QtCore
from PySide2.QtCore import Qt
import PySide2.QtWidgets as QtWidgets


class MainWindow(QtWidgets.QMainWindow):
    """Main window, with one button for exporting stuff"""

    def __init__(self, parent=None):
        super().__init__(parent)
        central_widget = QtWidgets.QWidget(self)
        layout = QtWidgets.QHBoxLayout(self)
        button = QtWidgets.QPushButton("Press me...")
        button.clicked.connect(self.export_stuff)
        layout.addWidget(button)
        central_widget.setLayout(layout)
        self.setCentralWidget(central_widget)

    def export_stuff(self):
        """Opens dialog and starts exporting"""
        some_window = MyExportDialog(self)
        some_window.exec_()


class MyAbstractExportThread(QtCore.QThread):
    """Base export thread"""
    change_value = QtCore.Signal(int)

    def run(self):
        cnt = 0
        while cnt < 100:
            cnt += 1
            self.operation()
            self.change_value.emit(cnt)

    def operation(self):
        pass


class MyExpensiveExportThread(MyAbstractExportThread):

    def operation(self):
        """Something that takes a lot of CPU power"""
        some_val = 0
        for i in range(1000000):
            some_val += 1


class MyInexpensiveExportThread(MyAbstractExportThread):

    def operation(self):
        """Something that doesn't take a lot of CPU power"""
        time.sleep(.1)


class MyExportDialog(QtWidgets.QDialog):
    """Dialog which does some stuff, and shows its progress"""

    def __init__(self, parent=None):
        super().__init__(parent, Qt.WindowCloseButtonHint)
        self.setWindowTitle("Exporting...")
        layout = QtWidgets.QHBoxLayout()
        self.progress_bar = self._create_progress_bar()
        layout.addWidget(self.progress_bar)
        self.setLayout(layout)
        self.worker = MyInexpensiveExportThread()  # Works fine
        # self.worker = MyExpensiveExportThread()  # Super laggy
        self.worker.change_value.connect(self.progress_bar.setValue)
        self.worker.start()
        self.worker.finished.connect(self.close)

    def _create_progress_bar(self):
        progress_bar = QtWidgets.QProgressBar(self)
        progress_bar.setMinimum(0)
        progress_bar.setMaximum(100)
        return progress_bar


if __name__ == "__main__":
    app = QtWidgets.QApplication()
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

推荐答案

谢谢 oetzi.这效果更好,但仍然在某种程度上拖累了 UI.我做了一些研究,并找到了以下内容,供感兴趣的人使用.

Thanks oetzi. This works better, but still drags the UI down somewhat. I did some research, and found the following, for those who are interested.

在使用线程运行计算繁重的进程时显示响应式用户界面的困难源于在这种情况下将所谓的 IO 密集型线程(即 GUI)与 CPU 密集型线程相结合的事实(即计算).对于受 IO 限制的进程,完成所需的时间由线程必须等待输入或输出(例如用户点击事物或计时器)这一事实来定义.相比之下,完成受 CPU 限制的进程所需的时间受执行该进程的处理单元的能力限制.

The difficulty with showing a responsive user-interface while running a computationally heavy process using threading, stems from the fact in this case one combines a so-called IO-bound thread (i.e. the GUI), with a CPU-bound thread (i.e. the computation). For a IO-bound process, the time it takes to complete is defined by the fact that the thread has to wait on input or output (e.g. a user clicking on things, or a timer). By contrast, the time required to finish a CPU-bound process is limited by the power of the processing unit performing the process.

原则上,在 Python 中混合这些类型的线程应该不成问题.尽管 GIL 强制在单个实例中只有一个线程在运行,但操作系统实际上将进程拆分为更小的指令,并在它们之间切换.如果一个线程正在运行,它有 GIL 并执行它的一些指令.在一段固定的时间后,它需要释放 GIL.一旦发布,GIL 可以调度激活任何其他可运行"线程——包括刚刚发布的线程.

In principle, mixing these types of threads in Python should not be a problem. Although the GIL enforces that only one thread is running at a single instance, the operating system in fact splits the processes up into smaller instructions, and switches between them. If a thread is running, it has the GIL and executes some of its instructions. After a fixed amount of time, it needs to release the GIL. Once released, the GIL can schedule activate any other 'runnable' thread - including the one that was just released.

然而,问题在于这些线程的调度.在这里事情对我来说有点模糊,但基本上发生的事情是 CPU 绑定线程似乎主宰了这个选择,从我可以收集到的信息是由于一个称为传送效应"的过程.因此,当在后台运行受 CPU 限制的线程时,Qt GUI 的不稳定和不可预测的行为.

The problem however, is with the scheduling of these threads. Here things become a bit fuzzy for me, but basically what happens is that the CPU-bound thread seems to dominate this selection, from what I could gather due to a process called the "convey effect". Hence, the erratic and unpredictable behavior of a Qt GUI when running a CPU-bound thread in the background.

我发现了一些有趣的阅读材料:

I found some interesting reading material on this:

更深入地分析 GIL

很好的线程调度可视化表示

所以...这非常好,我们如何解决这个问题?

So... this is very nice and all, how do we fix this?

最后,我设法使用多处理获得了我想要的东西.这允许您实际运行与 GUI 并行的进程,而不是按顺序运行.这可确保 GUI 保持响应速度,就像在后台没有受 CPU 限制的进程一样.

In the end, I managed to get what I want using multiprocessing. This allows you to actually run a process parallel to the GUI, instead in sequential fashion. This ensures the GUI stays as responsive as it would be without the CPU-bound process in the background.

多处理本身有很多困难,例如,在进程之间来回发送信息是通过跨管道发送腌制对象来完成的.但是,在我的情况下,最终结果确实更好.

Multiprocessing has a lot of difficulties of its own, for example the fact that sending information back and forth between processes is done by sending pickled objects across a pipeline. However, the end-result is really superior in my case.

下面我放了一个代码片段,展示了我的解决方案.它包含一个名为 ProgressDialog 的类,它提供了一个简单的 API,用于使用您自己的 CPU 绑定进程进行设置.

Below I put a code snippet, showing my solution. It contains a class called ProgressDialog, which provides an easy API for setting this up with your own CPU-bound process.

"""Contains class for executing a long running process (LRP) in a separate
process, while showing a progress bar"""

import multiprocessing as mp

from PySide2 import QtCore
from PySide2.QtCore import Qt
import PySide2.QtWidgets as QtWidgets


class ProgressDialog(QtWidgets.QDialog):
    """Dialog which performs a operation in a separate process, shows a
    progress bar, and returns the result of the operation

    Parameters
    ----
    title: str
        Title of the dialog
    operation: callable
        Function of the form f(conn, *args) that will be run
    args: tuple
        Additional arguments for operation
    parent: QWidget
        Parent widget

    Returns
    ----
    result: int
        The result is an integer. A 0 represents successful completion, or
        cancellation by the user. Negative numbers represent errors. -999
        is reserved for any unforeseen uncaught error in the operation.

    Examples
    ----
    The function passed as the operation parameter should be of the form
    ``f(conn, *args)``. The conn argument is a Connection object, used to
    communicate the progress of the operation to the GUI process. The
    operation can pass its progress with a number between 0 and 100, using
    ``conn.send(i)``. Once the process is finished, it should send 101.
    Error handling is done by passing negative numbers.

    >>> def some_function(conn, *args):
    >>>     conn.send(0)
    >>>     a = 0
    >>>     try:
    >>>         for i in range(100):
    >>>                 a += 1
    >>>                 conn.send(i + 1)  # Send progress
    >>>     except Exception:
    >>>         conn.send(-1)  # Send error code
    >>>     else:
    >>>         conn.send(101)  # Send successful completion code

    Now we can use an instance of the ProgressDialog class within any 
    QtWidget to execute the operation in a separate process, show a progress 
    bar, and print the error code:

    >>> progress_dialog = ProgressDialog("Running...", some_function, self)
    >>> progress_dialog.finished.connect(lambda err_code: print(err_code))
    >>> progress_dialog.open()
    """

    def __init__(self, title, operation, args=(), parent=None):
        super().__init__(parent, Qt.WindowCloseButtonHint)
        self.setWindowTitle(title)
        self.progress_bar = QtWidgets.QProgressBar(self)
        self.progress_bar.setValue(0)
        layout = QtWidgets.QHBoxLayout()
        layout.addWidget(self.progress_bar)
        self.setLayout(layout)

        # Create connection pipeline
        self.parent_conn, self.child_conn = mp.Pipe()

        # Create process
        args = (self.child_conn, *args)
        self.process = mp.Process(target=operation, args=args)

        # Create status emitter
        self.progress_emitter = ProgressEmitter(self.parent_conn, self.process)
        self.progress_emitter.signals.progress.connect(self.slot_update_progress)
        self.thread_pool = QtCore.QThreadPool()

    def slot_update_progress(self, i):
        if i < 0:
            self.done(i)
        elif i == 101:
            self.done(0)
        else:
            self.progress_bar.setValue(i)

    def open(self):
        super().open()
        self.process.start()
        self.thread_pool.start(self.progress_emitter)

    def closeEvent(self, *args):
        self.progress_emitter.running = False
        self.process.terminate()
        super().closeEvent(*args)


class ProgressEmitter(QtCore.QRunnable):
    """Listens to status of process"""

    class ProgressSignals(QtCore.QObject):
        progress = QtCore.Signal(int)

    def __init__(self, conn, process):
        super().__init__()
        self.conn = conn
        self.process = process
        self.signals = ProgressEmitter.ProgressSignals()
        self.running = True

    def run(self):
        while self.running:
            if self.conn.poll():
                progress = self.conn.recv()
                self.signals.progress.emit(progress)
                if progress < 0 or progress == 101:
                    self.running = False
            elif not self.process.is_alive():
                self.signals.progress.emit(-999)
                self.running = False

这篇关于在 Qt 中显示进度条,后台进程计算量很大的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆