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

Python 将stdout和stderr从辅助线程重定向到PyQt4 QTextEdit,python,multithreading,thread-safety,pyqt,stdout,Python,Multithreading,Thread Safety,Pyqt,Stdout,堆栈溢出。再一次,我来到你们面前,是在一个极度需要的时刻,摇摇欲坠,濒临疯狂的边缘。这个问题——从标题中可以明显看出——是我在这里看到的其他几个问题的综合 我有一个PyQt应用程序,我想立即将stdout和stderr流重新路由到GUI中的QTextEdit。 最初,我发现以下堆栈溢出答案: 这非常有效,但有一个警告:如果在CPU处理相对较长的方法时多次更新stdout或stderr,当主线程返回到应用程序循环时,所有更新都会同时显示。不幸的是,我有一些方法需要20秒才能完成(与网络相关),因此

堆栈溢出。再一次,我来到你们面前,是在一个极度需要的时刻,摇摇欲坠,濒临疯狂的边缘。这个问题——从标题中可以明显看出——是我在这里看到的其他几个问题的综合

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

最初,我发现以下堆栈溢出答案:

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

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

这就是它变得非常沮丧的地方:Python正在崩溃,因为使用上面包含的链接中的类,我已经将sys.stdout/err流分配给了QTextEdit小部件PyQt小部件不能从应用程序线程以外的任何线程进行修改,因为对stdout和stderr的更新来自我创建的辅助工作线程,它们违反了这条规则。我已经注释掉了重定向输出流的代码部分,果然,程序运行正常


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

首先,+1用于了解线程如何不安全。
堆栈溢出的许多示例都是如此

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

希望这是清楚的,如果不是,请随时提问

编辑:请注意,提供的代码示例没有很好地清理QThreads,因此退出时会打印警告。我将让您扩展到您的用例并清理线程


我还应该提到,有可能让您的
WriteStream
直接将事件发布到主线程。理论上,您可以使用
QApplication.postEvent()
将新构造的事件发布到主线程中您创建的
QObject
,该对象将更新文本框。不幸的是,
QApplication.postEvent
泄漏了PySide()中的内存,因此我更喜欢使用中间线程,以便它与PySide代码保持兼容。我认为在我发布的示例中,更容易理解正在发生的事情;我将使用你的方法,因为它对我来说非常有意义。我承认,我对python中的多线程没有太多的经验。。。将来,如果一个python模块或类是线程安全的,那么这是否意味着我可以跨多个线程操作它(如果需要,使用Locks())?如果一个python模块是线程安全的,那么您可以从多个没有锁的线程中使用它。我有点不确定一个线程安全的模块是否意味着用它创建的对象也是线程安全的。使用
Lock()
(例如我这样使用的库是h5py、zeromq、pandas),大多数东西都可以实现线程安全,但显然不是所有库都可以通过锁实现线程安全(aka Qt不能)。你真的需要逐案处理,并询问库的用户/开发人员。好的,谢谢。因为我已经实现了您的答案,所以实际上我在为日志记录创建的类中使用了RLock()(只能由锁定它的同一线程解锁)。很漂亮@凯特,这是个好问题。建议检查特定对象或字符串的
get()
调用(在
run
方法中)的结果,并适当地中断while循环。由于队列是线程安全的,您可以在退出时从主线程将对象/字符串/任何内容放入队列中。
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_()