问题:
- 在不锁定 GUI(“无响应”)的情况下跟踪线程进度的最佳做法是什么?
- 一般来说,线程在 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,他的解决方案将是我的选择。