34

Stack overflow. Once again, I come to you in a time of dire need, teetering precariously on the brink of insanity. This question - as may be evident from the title - is an amalgamation of several other questions I have seen answered here.

I have a PyQt application, and I want to re-route the stdout and stderr streams to a QTextEdit that is in my GUI without delay.

Initially, I found the following stack overflow answer: https://stackoverflow.com/a/17145093/629404

This works perfectly, but with one caveat: If stdout or stderr are updated multiple times while the CPU is processing a relatively longer method, all of the updates show up simultaneously when the main thread returns to the application loop. Unfortunately, I have a few methods which take up to 20 seconds to complete (networking related), and so the application becomes unresponsive - and the QTextEdit does not update - until they are finished.

In order to fix this problem, I delegated all of the GUI processing to the main thread, and I have been spawning off a second thread to handle the longer networking operations, using pyqtSignals to notify the main thread of when the work is finished and pass back results. Immediately when I began testing the code written this way, the python interpreter began crashing without any warning.

This is where it gets very frusterating: Python is crashing because - using the class from the included link above - I have assigned the sys.stdout/err streams to the QTextEdit widget; PyQt widgets cannot be modified from any thread other then the application thread, and since the updates to stdout and stderr are coming from the secondary worker thread that I created, they are violating this rule. I have commented out the section of code where I redirect the output streams, and sure enough, the program runs without error.

This brings me back to square one, and leaves me in a confusing situation; Assuming I continue to handle GUI related operations in the main thread and deal with computation and longer operations in a secondary thread (which I have come to understand is the best way to keep the application from blocking when the user triggers events), how can I redirect Stdout and Stderr from both threads to the QTextEdit widget? The class in the link above works just fine for the main thread, but kills python - for the reason described above - when updates come from the second thread.

Community
  • 1
  • 1
araisbec
  • 1,223
  • 3
  • 16
  • 27

1 Answers1

33

Firstly, +1 for realising how thread-unsafe many of the examples on stack overflow are!

The solution is to use a thread-safe object (like a Python Queue.Queue) to mediate the transfer of information. I've attached some sample code below which redirects stdout to a Python Queue. This Queue is read by a QThread, which emits the contents to the main thread through Qt's signal/slot mechanism (emitting signals is thread-safe). The main thread then writes the text to a text edit.

Hope that is clear, feel free to ask questions if it is not!

EDIT: Note that the code example provided doesn't clean up QThreads nicely, so you'll get warnings printed when you quit. I'll leave it to you to extend to your use case and clean up the thread(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_()
three_pineapples
  • 11,579
  • 5
  • 38
  • 75
  • I should also mention that there is the possibility of having your `WriteStream` directly posting events to the main thread. In theory you can use `QApplication.postEvent()` to post a freshly constructed event to a `QObject` of your making which resides in the MainThread, which will update the textbox. Unfortunately, `QApplication.postEvent` leaks memory in PySide (https://bugreports.qt-project.org/browse/PYSIDE-205) so I prefer to use an intermediate thread so that it remains compatible with PySide code. I think it is also easier to follow what is going on in my posted example. – three_pineapples Jan 12 '14 at 06:47
  • 1
    Thank you so much for the detailed answer; I am going to use your approach, as it makes perfect sense to me. I confess, I am not terribly experienced with multi-threading in python... In the future, if a python module or class is thread-safe, then does that mean that I can manipulate it across multiple threads (with Locks() if necessary)? – araisbec Jan 12 '14 at 15:45
  • If a python module is thread-safe then you should be fine to use it from multiple threads without locks. I'm a little unsure whether a module being thread-safe implies objects created with it are also thread-safe. Most things can be made thread-safe using a `Lock()` (for instance libraries I use like this are h5py, zeromq, pandas) but obviously not all libraries can be made thread safe with locks (aka Qt can't). You really have to treat it on a case-by-case basis and ask the users/developers of libraries. – three_pineapples Jan 12 '14 at 22:30
  • Ok, thanks. Since I've implemented your answer, I've actually gone on to use RLock() within a class I've created for logging (can only be unlocked by the same thread that locked it). Pretty nifty! – araisbec Jan 19 '14 at 21:50
  • Thank you for giving an elaborate example. However, when I click on button "start long running thread", nothing happens. Tried a print command from start_thread, that does not show. – nish Dec 29 '14 at 11:09
  • Hi @nish, you should post a new question on stack overflow (tag it with pyqt, python etc). It will be too hard to debug in comments here. Make sure to include the exact code you are trying to run and also link back to this answer for reference. Cheers, – three_pineapples Dec 29 '14 at 23:07
  • Seeing that `self.queue.get()` blocks, how could the thread be terminated on quit? – Kar Feb 25 '15 at 10:17
  • 1
    @Kate That's a good question. One suggestion would be to check the result of the `get()` call (in the `run` method) for a specific object or string, and break out of the while loop appropriately. As the queue is thread safe, you could place the object/string/whatever in the queue from the main thread on quit. – three_pineapples Feb 25 '15 at 10:33
  • @three_pineapples Right. I guess it could push an instance of a dummy class into the queue. Alternatively, albeit its undesirability, letting the thread crash on quit shouldn't be detrimental right? The thread is technically read-only and it should be fairly harmless to let it crash? – Kar Feb 25 '15 at 11:37
  • @Kate yep I see no problem with that either. – three_pineapples Feb 25 '15 at 20:11
  • I used set the stream argument of StreamHandler to WriteStream(queue) and it worked beutifaully – Mattwmaster58 Apr 10 '18 at 04:36
  • @three_pineapples I am trying to make your solution work on my code. I made a new question for it. I would appreciate your help. https://stackoverflow.com/questions/50077356/redirecting-stdout-from-a-secondary-thread-multithreading-with-a-function-inste – shafuq Apr 28 '18 at 14:24