20

问题:

  1. 在不锁定 GUI(“无响应”)的情况下跟踪线程进度的最佳做法是什么?
  2. 一般来说,线程在 GUI 开发中的最佳实践是什么?

问题背景:

  • 我有一个适用于 Windows 的 PyQt GUI。
  • 它用于处理 HTML 文档集。
  • 处理一组文档需要三秒到三个小​​时不等。
  • 我希望能够同时处理多个集合。
  • 我不希望 GUI 锁定。
  • 我正在寻找线程模块来实现这一点。
  • 我对线程比较陌生。
  • GUI 有一个进度条。
  • 我希望它显示所选线程的进度。
  • 如果已完成,则显示所选线程的结果。
  • 我正在使用 Python 2.5。

我的想法:让线程在更新进度时发出 QtSignal 触发一些更新进度条的功能。完成处理时也会发出信号,以便显示结果。

#NOTE: this is example code for my idea, you do not have
#      to read this to answer the question(s).

import threading
from PyQt4 import QtCore, QtGui
import re
import copy

class ProcessingThread(threading.Thread, QtCore.QObject):

    __pyqtSignals__ = ( "progressUpdated(str)",
                        "resultsReady(str)")

    def __init__(self, docs):
        self.docs = docs
        self.progress = 0   #int between 0 and 100
        self.results = []
        threading.Thread.__init__(self)

    def getResults(self):
        return copy.deepcopy(self.results)

    def run(self):
        num_docs = len(self.docs) - 1
        for i, doc in enumerate(self.docs):
            processed_doc = self.processDoc(doc)
            self.results.append(processed_doc)
            new_progress = int((float(i)/num_docs)*100)
            
            #emit signal only if progress has changed
            if self.progress != new_progress:
                self.emit(QtCore.SIGNAL("progressUpdated(str)"), self.getName())
            self.progress = new_progress
            if self.progress == 100:
                self.emit(QtCore.SIGNAL("resultsReady(str)"), self.getName())
    
    def processDoc(self, doc):
        ''' this is tivial for shortness sake '''
        return re.findall('<a [^>]*>.*?</a>', doc)


class GuiApp(QtGui.QMainWindow):
    
    def __init__(self):
        self.processing_threads = {}  #{'thread_name': Thread(processing_thread)}
        self.progress_object = {}     #{'thread_name': int(thread_progress)}
        self.results_object = {}      #{'thread_name': []}
        self.selected_thread = ''     #'thread_name'
        
    def processDocs(self, docs):
        #create new thread
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()
        
        #add thread to dict of threads
        self.processing_threads[thread_name] = p_thread
        
        #init progress_object for this thread
        self.progress_object[thread_name] = p_thread.progress  
        
        #connect thread signals to GuiApp functions
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('progressUpdated(str)'), self.updateProgressObject(thread_name))
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('resultsReady(str)'), self.updateResultsObject(thread_name))
        
    def updateProgressObject(self, thread_name):
        #update progress_object for all threads
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress
        
        #update progress bar for selected thread
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])
        
    def updateResultsObject(self, thread_name):
        #update results_object for thread with results
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()
        
        #update results widget for selected thread
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

对此方法的任何评论(例如缺点、陷阱、赞美等)都将受到赞赏。

解析度:

我最终使用 QThread 类和相关的信号和插槽在线程之间进行通信。这主要是因为我的程序已经将 Qt/PyQt4 用于 GUI 对象/小部件。该解决方案还需要对我现有的代码进行较少的更改来实现。

这是一篇适用的 Qt 文章的链接,该文章解释了 Qt 如何处理线程和信号,http://www.linuxjournal.com/article/9602。摘录如下:

幸运的是,Qt 允许跨线程连接信号和槽——只要线程运行它们自己的事件循环。与发送和接收事件相比,这是一种更简洁的通信方法,因为它避免了所有重要的应用程序中都需要的所有簿记和中间 QEvent 派生类。线程之间的通信现在变成了将信号从一个线程连接到另一个线程的槽的问题,线程之间交换数据的互斥和线程安全问题由 Qt 处理。

为什么有必要在要连接信号的每个线程中运行一个事件循环?原因与 Qt 在将信号从一个线程连接到另一个线程的插槽时使用的线程间通信机制有关。当建立这样的连接时,它被称为排队连接。当信号通过排队连接发出时,下一次执行目标对象的事件循环时调用插槽。如果槽被另一个线程的信号直接调用,则该槽将在与调用线程相同的上下文中执行。通常,这不是您想要的(尤其是如果您使用数据库连接,则不是您想要的,因为数据库连接只能由创建它的线程使用)。排队的连接将信号正确地分派给线程对象,并通过搭载事件系统在其自己的上下文中调用其槽。这正是我们想要的线程间通信,其中一些线程正在处理数据库连接。Qt 信号/槽机制在根本上是上述线程间事件传递方案的实现,但具有更清洁和更易于使用的接口。

注意: eliben也有一个很好的答案,如果我不使用处理线程安全和互斥的 PyQt4,他的解决方案将是我的选择。

4

6 回答 6

9

如果你想使用信号来指示主线程的进度,那么你真的应该使用 PyQt 的 QThread 类而不是 Python 线程模块中的 Thread 类。

PyQt Wiki 上有一个使用 QThread、信号和槽的简单示例:

https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots

于 2009-02-22T01:33:00.037 回答
5

本机 python 队列将不起作用,因为您必须阻塞队列 get(),这会破坏您的 UI。

Qt本质上在内部实现了一个队列系统,用于跨线程通信。尝试从任何线程调用此调用以发布对插槽的调用。

QtCore.QMetaObject.invokeMethod()

它很笨重,文档记录也很差,但它应该可以做你想做的事情,即使是来自非 Qt 线程。

您也可以为此使用事件机制。有关名为“post”之类的方法,请参见 QApplication(或 QCoreApplication)。

编辑:这是一个更完整的例子......

我基于 QWidget 创建了自己的类。它有一个接受字符串的槽;我这样定义它:

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...

稍后,我在主 GUI 线程中创建了这个小部件的实例。从主 GUI 线程或任何其他线程(敲木头)我可以调用:

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))

笨重,但它能让你到达那里。

担。

于 2009-06-19T21:21:59.003 回答
4

我建议您使用队列而不是信号。就我个人而言,我发现它是一种更加健壮和易于理解的编程方式,因为它更加同步。

线程应该从队列中获取“作业”,并将结果放回另一个队列。然而,线程可以使用第三个队列来获取通知和消息,例如错误和“进度报告”。一旦以这种方式构建代码,管理起来就会变得更加简单。

这样,一组工作线程也可以使用单个“作业队列”和“结果队列”,它将来自线程的所有信息路由到主 GUI 线程。

于 2009-02-21T07:18:06.127 回答
1

如果您的方法“processDoc”没有更改任何其他数据(只是查找一些数据并返回它并且不更改父类的变量或属性),您可以在其中使用 Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_THREADS 宏(有关详细信息,请参见此处)。因此文档将在不会锁定解释器的线程中处理,并且 UI 将被更新。

于 2009-02-21T05:44:14.730 回答
1

下面是一个基本的 PyQt5/PySide2 示例,展示了如何在更新进度条的同时运行后台任务。该任务被移至工作线程,并使用自定义信号与主 GUI 线程进行通信。任务可以停止和重新启动,并在窗口关闭时自动终止。

# from PySide2 import QtCore, QtWidgets
#
# class Worker(QtCore.QObject):
#     progressChanged = QtCore.Signal(int)
#     finished = QtCore.Signal()

from PyQt5 import QtCore, QtWidgets

class Worker(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    finished = QtCore.pyqtSignal()

    def __init__(self):
        super().__init__()
        self._stopped = True

    def run(self):
        count = 0
        self._stopped = False
        while count < 100 and not self._stopped:
            count += 5
            QtCore.QThread.msleep(250)
            self.progressChanged.emit(count)
        self._stopped = True
        self.finished.emit()

    def stop(self):
        self._stopped = True

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.button = QtWidgets.QPushButton('Start')
        self.button.clicked.connect(self.handleButton)
        self.progress = QtWidgets.QProgressBar()
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.progress)
        layout.addWidget(self.button)
        self.thread = QtCore.QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.handleFinished)
        self.worker.progressChanged.connect(self.progress.setValue)
        self.thread.started.connect(self.worker.run)

    def handleButton(self):
        if self.thread.isRunning():
            self.worker.stop()
        else:
            self.button.setText('Stop')
            self.thread.start()

    def handleFinished(self):
        self.button.setText('Start')
        self.thread.quit()

    def closeEvent(self, event):
        self.worker.stop()
        self.thread.quit()
        self.thread.wait()

if __name__ == '__main__':

    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setWindowTitle('Threaded Progress')
    window.setGeometry(600, 100, 250, 50)
    window.show()
    sys.exit(app.exec_())
于 2021-08-26T12:52:23.007 回答
0

在 Python 中你总是会遇到这个问题。谷歌 GIL “全局解释器锁”了解更多背景信息。有两种通常推荐的方法来解决您遇到的问题:使用Twisted或使用类似于2.5 中引入的多处理模块的模块。

Twisted 将要求您学习异步编程技术,这在开始时可能会令人困惑,但如果您需要编写高吞吐量的网络应用程序会很有帮助,并且从长远来看对您更有利。

多处理模块将派生一个新进程并使用 IPC 使其表现得就像您拥有真正的线程一样。唯一的缺点是您需要安装相当新的 python 2.5,它默认包含在大多数 Linux 发行版或 OSX 中。

于 2009-02-20T18:43:44.160 回答