将stdout和stderr从辅助线程重定向到PyQt4 QTextEdit

34

堆栈溢出。我再次在你这里寻求帮助,岌岌可危地悬在疯狂的边缘上。正如标题所示,这个问题是我在这里看到的几个其他问题的综合。

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

最初,我找到了以下堆栈溢出答案:https://dev59.com/P3PYa4cB1Zd3GeqPh1_c#17145093

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

为了解决这个问题,我将所有GUI处理委托给主线程,并使用pyqtSignals生成第二个线程来处理更长的网络操作,以通知主线程何时完成工作并传回结果。但是,当我开始测试这种编写方式的代码时,Python解释器开始在没有任何警告的情况下崩溃。
这就是非常令人沮丧的地方:Python崩溃是因为 - 使用上面链接中的类 - 我已经将sys.stdout/err流分配给了QTextEdit小部件; PyQt小部件不能从应用程序线程以外的任何线程修改,而由于stdout和stderr的更新来自我创建的辅助工作线程,它们违反了此规则。我已经注释掉了重定向输出流的代码部分,确实,程序运行时没有错误。

这让我回到了原点,让我陷入了困惑的境地;假设我继续在主线程中处理GUI相关操作,并在辅助线程中处理计算和长时间操作(我已经了解到这是保持应用程序不会在用户触发事件时阻塞的最佳方法),那么如何将来自两个线程的Stdout和Stderr重定向到QTextEdit小部件?上面链接中的类对于主线程工作得很好,但当更新来自第二个线程时,它会导致Python崩溃 - 原因如上所述。

1个回答

34
首先,加1表示认识到了Stack Overflow上许多示例是线程不安全的!
解决方法是使用线程安全对象(例如Python Queue.Queue)来调节信息传输。以下是一些示例代码,将stdout重定向到Python Queue。 这个Queue由一个QThread读取,通过Qt的信号/槽机制将内容发射到主线程中(发射信号是线程安全的)。 主线程然后将文本写入到文本编辑器中。
希望这样清楚,如果不清楚,请随时提问!
编辑:请注意,提供的代码示例没有很好地清理QThreads,因此在退出时会打印警告。 我会留给你扩展并清理线程(s)的用例
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() 将新构造的事件发布到您自己创建并驻留在MainThread中的 QObject,这将更新文本框。不幸的是,QApplication.postEvent 在 PySide 中会泄漏内存(https://bugreports.qt-project.org/browse/PYSIDE-205),因此我更喜欢使用中介线程,以使其与 PySide 代码兼容。我认为,在我的示例中跟踪发生的事情也更容易。 - three_pineapples
1
非常感谢您提供详细的答案;我会采用您的方法,因为对我来说非常合理。我承认,在Python中并不是非常熟悉多线程... 将来,如果一个Python模块或类是线程安全的,那么这是否意味着我可以跨多个线程进行操作(如有必要使用Locks())? - araisbec
如果一个Python模块是线程安全的,那么你应该可以在多个线程中使用它而不需要锁。我有点不确定一个模块是否是线程安全意味着用它创建的对象也是线程安全的。大多数东西都可以使用Lock()来实现线程安全(例如我使用的库如h5py、zeromq、pandas),但显然并非所有库都可以使用锁来实现线程安全(比如Qt就不行)。你真的必须根据情况具体分析,并询问库的用户/开发者。 - three_pineapples
好的,谢谢。自从我实施了你的答案后,我实际上已经在我创建的记录类中使用了RLock()(只能由锁住它的同一线程解锁)。相当不错! - araisbec
1
@Kate 这是一个很好的问题。一个建议是,在run方法中检查get()调用(对于特定的对象或字符串)的结果,并适当地跳出while循环。由于队列是线程安全的,你可以在主线程上退出时将对象/字符串/任何其他内容放入队列中。 - three_pineapples
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接