如何在不冻结PyQt GUI的情况下跟踪Python中的线程进度?

时间:2009-02-20 14:00:06

标签: python multithreading user-interface pyqt

问题:

  1. 最佳做法是什么? 跟踪胎面 没有锁定GUI的进度 (“没有回应”)?
  2. 一般来说,最佳做法是什么? 适用于GUI的线程 发展?
  3. 问题背景:

    • 我有一个适用于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使用的通信机制   从一个连接信号时   线程到另一个线程的插槽。   当建立这样的连接时,它就是   称为排队连接。   当信号通过一个发射   排队连接,调用插槽   下次目标对象的时候   事件循环被执行。如果插槽   而是由a直接调用   来自另一个线程的信号,那个槽   将在与上述相同的上下文中执行   调用线程。通常,这是   不是你想要的(尤其不是   你想要的是什么?   数据库连接,作为数据库   连接只能由   创建它的线程)。排队   连接正确发送   发信号到线程对象和   通过它在自己的上下文中调用它的槽   捎带事件系统。   这正是我们想要的   线程间通信   一些线程正在处理   数据库连接。 Qt   信号/插槽机制是根源   线程间的实现   上面概述的事件传递方案,   但有一个更清洁和   易于使用的界面。

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

5 个答案:

答案 0 :(得分:10)

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

可以在PyQt Wiki上找到使用QThread,信号和插槽的简单示例:

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

答案 1 :(得分:5)

原生python队列不起作用,因为你必须阻塞队列get(),这会阻塞你的UI。

Qt基本上在内部实现了一个排队系统,用于跨线程通信。从任何线程尝试此调用以将呼叫发布到插槽。

QtCore.QMetaObject.invokeMethod()

它很笨拙而且记录很少,但它应该从非Qt线程中做你想要的。

您也可以使用事件机制。请参阅QApplication(或QCoreApplication)以获取名为“post”的方法。

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

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

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

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

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))
笨拙,但它会让你到那里。

答案 2 :(得分:4)

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

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

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

答案 3 :(得分:1)

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

答案 4 :(得分:0)

你总是会在Python中遇到这个问题。谷歌GIL“全球解释锁定”为更多背景。通常有两种方法可以解决您遇到的问题:使用Twisted,或使用类似于2.5中引入的multiprocessing模块的模块。

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

多处理模块将分叉一个新进程并使用IPC使其行为就像您有真正的线程一样。唯一的缺点是你需要安装python 2.5,这是相当新的,并且默认情况下包含在大多数Linux发行版或OSX中。