【问题标题】:Redirecting stdout and stderr to a PyQt4 QTextEdit from a secondary thread将 stdout 和 stderr 从辅助线程重定向到 PyQt4 QTextEdit
【发布时间】:2014-01-31 00:39:53
【问题描述】:

堆栈溢出。再一次,我在急需的时候来找你,在精神错乱的边缘摇摇欲坠。这个问题——从标题中可以看出——是我在这里看到的其他几个问题的合并。

我有一个 PyQt 应用程序,我想将 stdout 和 stderr 流重新路由到我的 GUI 中的 QTextEdit没有延迟

最初,我找到了以下堆栈溢出的答案: https://stackoverflow.com/a/17145093/629404

这很完美,但有一个警告:如果在 CPU 处理相对较长的方法时多次更新 stdout 或 stderr,当主线程返回应用程序循环时,所有更新都会同时显示。不幸的是,我有一些方法最多需要 20 秒才能完成(与网络相关),因此应用程序变得无响应 - 并且 QTextEdit 不会更新 - 直到它们完成。

为了解决这个问题,我将所有的 GUI 处理委托给主线程,并且我一直在生成第二个线程来处理更长的网络操作,使用 pyqtSignals 来通知主线程何时工作完成并传回结果。当我开始测试以这种方式编写的代码时,python 解释器立即开始崩溃,没有任何警告。

这是非常令人沮丧的地方:Python 崩溃了,因为 - 使用上面包含的链接中的类 - 我已将 sys.stdout/err 流分配给 QTextEdit 小部件; PyQt 小部件不能从除应用程序线程之外的任何线程进行修改,并且由于对 stdout 和 stderr 的更新来自我创建的辅助工作线程,因此它们违反了此规则。我已注释掉我重定向输出流的代码部分,果然,程序运行没有错误。

这让我回到了原点,让我陷入了混乱的境地;假设我继续在主线程中处理与 GUI 相关的操作,并在辅助线程中处理计算和更长的操作(我已经明白这是防止应用程序在用户触发事件时阻塞的最佳方法),我怎么能将两个线程的 Stdout 和 Stderr 重定向到 QTextEdit 小部件?上面链接中的类在主线程中工作得很好,但是当更新来自第二个线程时,由于上述原因,杀死了 python。

【问题讨论】:

    标签: python multithreading thread-safety pyqt stdout


    【解决方案1】:

    首先,+1 用于实现线程不安全的许多堆栈溢出示例!

    解决方案是使用线程安全对象(如 Python Queue.Queue)来调解信息传输。我在下面附加了一些示例代码,它们将stdout 重定向到 Python Queue。这个QueueQThread 读取,它通过Qt 的信号/槽机制将内容发送到主线程(发出信号是线程安全的)。然后主线程将文本写入文本编辑器。

    希望这很清楚,如果不是,请随时提出问题!

    编辑:请注意,提供的代码示例不能很好地清理 QThreads,因此当您退出时会打印出警告。我将把它留给你来扩展你的用例并清理线程

    import sys
    from Queue import Queue
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    
    # The new Stream Object which replaces the default stream associated with sys.stdout
    # This object just puts data in a queue!
    class WriteStream(object):
        def __init__(self,queue):
            self.queue = queue
    
        def write(self, text):
            self.queue.put(text)
    
    # A QObject (to be run in a QThread) which sits waiting for data to come through a Queue.Queue().
    # It blocks until data is available, and one it has got something from the queue, it sends
    # it to the "MainThread" by emitting a Qt Signal 
    class MyReceiver(QObject):
        mysignal = pyqtSignal(str)
    
        def __init__(self,queue,*args,**kwargs):
            QObject.__init__(self,*args,**kwargs)
            self.queue = queue
    
        @pyqtSlot()
        def run(self):
            while True:
                text = self.queue.get()
                self.mysignal.emit(text)
    
    # An example QObject (to be run in a QThread) which outputs information with print
    class LongRunningThing(QObject):
        @pyqtSlot()
        def run(self):
            for i in range(1000):
                print i
    
    # An Example application QWidget containing the textedit to redirect stdout to
    class MyApp(QWidget):
        def __init__(self,*args,**kwargs):
            QWidget.__init__(self,*args,**kwargs)
    
            self.layout = QVBoxLayout(self)
            self.textedit = QTextEdit()
            self.button = QPushButton('start long running thread')
            self.button.clicked.connect(self.start_thread)
            self.layout.addWidget(self.textedit)
            self.layout.addWidget(self.button)
    
        @pyqtSlot(str)
        def append_text(self,text):
            self.textedit.moveCursor(QTextCursor.End)
            self.textedit.insertPlainText( text )
    
        @pyqtSlot()
        def start_thread(self):
            self.thread = QThread()
            self.long_running_thing = LongRunningThing()
            self.long_running_thing.moveToThread(self.thread)
            self.thread.started.connect(self.long_running_thing.run)
            self.thread.start()
    
    # Create Queue and redirect sys.stdout to this queue
    queue = Queue()
    sys.stdout = WriteStream(queue)
    
    # Create QApplication and QWidget
    qapp = QApplication(sys.argv)  
    app = MyApp()
    app.show()
    
    # Create thread that will listen on the other end of the queue, and send the text to the textedit in our application
    thread = QThread()
    my_receiver = MyReceiver(queue)
    my_receiver.mysignal.connect(app.append_text)
    my_receiver.moveToThread(thread)
    thread.started.connect(my_receiver.run)
    thread.start()
    
    qapp.exec_()
    

    【讨论】:

    • 我还应该提到,有可能让您的WriteStream 直接将事件发布到主线程。理论上,您可以使用QApplication.postEvent() 将新构建的事件发布到您制作的位于主线程中的QObject,这将更新文本框。不幸的是,QApplication.postEvent 泄漏了 PySide (bugreports.qt-project.org/browse/PYSIDE-205) 中的内存,所以我更喜欢使用中间线程,以便它与 PySide 代码保持兼容。我认为遵循我发布的示例中发生的事情也更容易。
    • 非常感谢您的详细回答;我将使用您的方法,因为它对我来说非常有意义。我承认,我在 python 中的多线程方面经验并不丰富......将来,如果 python 模块或类是线程安全的,那么这是否意味着我可以跨多个线程操作它(如果使用 Locks()必要)?
    • 如果一个 python 模块是线程安全的,那么你应该可以在多个线程中使用它而不用锁。我有点不确定一个模块是线程安全的是否意味着用它创建的对象也是线程安全的。大多数事情都可以使用Lock() 实现线程安全(例如我使用的库是h5py、zeromq、pandas),但显然并非所有库都可以通过锁实现线程安全(又名Qt 不能)。您确实必须逐案处理,并询问图书馆的用户/开发人员。
    • 好的,谢谢。由于我已经实现了你的答案,我实际上已经在我为日志记录创建的类中使用 RLock() (只能由锁定它的同一线程解锁)。很漂亮!
    • @Kate 这是个好问题。一种建议是检查特定对象或字符串的get() 调用(在run 方法中)的结果,并适当地跳出while 循环。由于队列是线程安全的,因此您可以在退出时从主线程将对象/字符串/任何内容放入队列中。
    猜你喜欢
    • 2014-07-22
    • 1970-01-01
    • 2012-08-07
    • 2022-01-18
    • 2012-07-14
    • 2013-07-22
    • 2011-05-23
    • 1970-01-01
    • 2020-06-02
    相关资源
    最近更新 更多