0

Currently writing a GUI application that launches from and interfaces with The Foundry's Nuke, a visual effects compositing software (although this is not the issue AFAIK, since it's repeatable inside an IDE). I am attempting to figure out why running one native Python thread in my Pyside2 GUI is crashing when calling a function that results in updating a QLabel, while another similar function that is calling the same function to update a different QLabel seems to work every time without issue. Both of these are threaded as they're background processes involving data I/O and API requests that can take some time. This is in Pyside2/Qt 5.12, and python 3.7, neither of which can be updated due to software limitations.

I've already read here that using QThread is usually "better" than the regular threading module for interacting with Qt, and here that "PyQt GUI calls like setText() on a QLineEdit are not allowed from a thread." I will likely switch to QThread if that is the solution, but there's a lot more data passing that I'd need to do after creating a separate Worker class, as is suggested by many tutorials I've seen, as well as this SO answer, so I'd like to keep it using the native threading module if at all possible because that seems less complicated.

However, it seemed a good idea to ask, as it seems like one of the buttons connected to a thread has the ability to update a QLabel just fine, and the other one consistently crashes, which doesn't make sense. Below is some stripped down code replicating the problem. The connectAPI function is for connecting to an external API that does some logic and processing for the current shot, simulated with a time.sleep(). (Also I am aware that my functions and variables are not PEP8 compliant, camelCase is the convention in this codebase)


# PySide2 / QT 5.12
# Python 3.7

import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
import PySide2.QtWidgets as QtWidgets

import time
import sys
import threading

class MyMainWindow(QtWidgets.QWidget):

    def __init__(self, pathToFile):
        super(MyMainWindow, self).__init__()
        self.resize(250, 200)
        self.connectionEstablished = False
        self.inputFile = pathToFile
        self.createWidgets()
        self.layoutWidgets()

    def startConnectionThread(self):
        self.connectionValid = False
        # This never shows in the UI, but does print from the updateStatus(), so presumably it's just immediately getting changed by the
        #   first updateStatus() call inside of connectAPI
        self.updateStatus(self.connectionLabel, 'inprogress', 'Attempting Connection')
        connectionThread = threading.Thread(target=self.connectAPI)
        connectionThread.start()

    def connectAPI(self):
        print("connectAPI function")
        # This works fine, even though it "shouldn't" because it's updating a QLabel from inside a thread
        self.updateStatus(self.connectionLabel, 'inprogress', "Searching...")
        # Simulated external API connection and processing
        self.connectionObject = True
        for x in range(0,11):
            print(x)
            time.sleep(0.2)

        if self.connectionObject == False:
            self.updateStatus(self.connectionLabel, 'failure', "Object Not Found!")
            return False
        else:
            self.objectFound = True
        # This works fine, even though it "shouldn't" because it's updating a QLabel from inside a thread
        self.updateStatus(self.connectionLabel, 'success', "Search Complete")

    def updateStatus(self, label, color, text):
        print(f"update status: {text}")
        if color == 'success':
            color = 'ForestGreen'
        if color == 'failure':
            color = 'OrangeRed'
        elif color == 'inprogress':
            color = 'Tan'
        label.setText('<FONT COLOR="{color}"><b>{text}<b></FONT>'.format(color=color, text=text))

    def submitButtonThread(self):
        print("starting submit thread")
        submit_thread = threading.Thread(target=self.testFunction)
        submit_thread.start()

    def testFunction(self):
        print("test function")
        for x in range(0, 11):
            print(x)
            time.sleep(0.2)

        # Uncommenting the following line will crash the GUI, but only in this function
        self.updateStatus(self.submitLabel, 'inprogress', 'test function end...')


    def createWidgets(self):
        # Widget - Verify Connection Button
        self.connectionGet = QtWidgets.QPushButton('Verify Name')
        self.connectionGet.clicked.connect(self.startConnectionThread)

        # Widget - Connection Success Label
        self.connectionLabel = QtWidgets.QLabel('')
        self.connectionLabel.sizeHint()
        self.connectionLabel.setAlignment(QtCore.Qt.AlignCenter)

        # Widget - Creation Button
        self.createSubmitButton = QtWidgets.QPushButton('Create Submission')
        self.createSubmitButton.clicked.connect(self.submitButtonThread)

        # Widget - Submit Success Label
        self.submitLabel = QtWidgets.QLabel('')
        self.submitLabel.sizeHint()
        self.submitLabel.setAlignment(QtCore.Qt.AlignCenter)
        self.submitLabel.setMaximumHeight(30)

    def layoutWidgets(self):
        self.mainLayout = QtWidgets.QVBoxLayout(self)

        self.connectionGroup = QtWidgets.QGroupBox('API Connection')
        connectionLayout = QtWidgets.QGridLayout()
        connectionLayout.addWidget(self.connectionGet, 0, 2, 1, 2)
        connectionLayout.addWidget(self.connectionLabel, 1, 0, 1, 4)
        self.connectionGroup.setLayout(connectionLayout)

        self.creationSection = QtWidgets.QGroupBox('Creation Section')
        creationLayout = QtWidgets.QGridLayout()
        creationLayout.addWidget(self.createSubmitButton, 0, 2, 1, 2)
        creationLayout.addWidget(self.submitLabel, 1, 0, 1, 4)
        self.creationSection.setLayout(creationLayout)


        # Add them all to main
        self.mainLayout.addWidget(self.connectionGroup)
        self.mainLayout.addSpacing(10)
        self.mainLayout.addWidget(self.creationSection)
        self.setLayout(self.mainLayout)


# This function is how it's called from inside Nuke, included for completeness
def launchGUI(pathToFile):
    global window
    window = MyMainWindow(pathToFile)
    window.show()


# This is used inside the IDE for testing
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    global window
    window = MyMainWindow("//Server/path/to/file.txt")
    window.show()
    sys.exit(app.exec_())

Output when clicking connectionGet button:

update status: Attempting Connection 
connectAPI function 
update status: Searching... 
0 
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
update status: Search Complete

Output when clicking createSubmitButton button:

starting submit thread 
test function 
0 
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
update status: test function end...

Process finished with exit code -1073741819 (0xC0000005)

(And it crashes with that exit code)

I tried swapping the threads targeted by the two functions to see if the results would be the same, thinking maybe only one set of threads/updates can happen, and got even more confusing results:

connectionThread = threading.Thread(target=self.testFunction)

Results in:

update status: Attempting Connection 
test function 
0 
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
update status: test function end...

Process finished with exit code -1073741819 (0xC0000005)

(Crash)

submit_thread = threading.Thread(target=self.connectAPI)

Results in:

starting submit thread 
connectAPI function 
update status: Searching... 
0 
1

Process finished with exit code -1073741819 (0xC0000005)

(Crash)

  • too much code, please reduce to this: https://stackoverflow.com/help/minimal-reproducible-example – D.L Oct 28 '22 at 18:18

1 Answers1

1

ANY update to QT GUI in python from any thread except the main thread is an unidentified behavior, it will most likely lead to segmentation fault, but in some cases it may not do it on that line, it may do it on a totally different line and you'll be left scratching your head for days, why it crashes here and not there ? try it on a different OS and it will crash there but not here, etc ..

keep all updates to GUI in your main thread and use signals emitted from children to run functions on your main thread that will update the GUI once a threaded function finishes execution, you can find an example in this SO question which this question is a duplicate of. Updating GUI elements in MultiThreaded PyQT

Ahmed AEK
  • 8,584
  • 2
  • 7
  • 23
  • Thanks! Looking into using QThreadPool to split the work off and create a signal from a Worker class using [this tutorial](https://www.pythonguis.com/tutorials/multithreading-pyside-applications-qthreadpool/) and in the caveats section, it mentions that database querying or IO should use pure python (concurrent futures)? – indoorjetpacks Oct 28 '22 at 18:57
  • @indoorjetpacks if you want to separate your query code to run on ... let's say a server, depending on QThreads will really tie you down, so it's best to just use pure python for threading unless you need a functionality from QThreads such as being able to shutdown the thread any time, so just use python's native threads, the choice of which to use depends on exact usage, so just get something working with any implementation of them. (concurrent.futures.threadpoolexecutor or multiprocessing.threadpool or thread.thread) – Ahmed AEK Oct 28 '22 at 19:04
  • So then how would I emit signals during a threaded function running to update the GUI? Looking at that example it looks like signals can only be passed at the end of an execution? Also in my example I was using pure python threading, and that's what caused the crashes when I tried to update the GUI - which lead me to discover the recommendations of using QThread/QRunnable – indoorjetpacks Oct 28 '22 at 23:14
  • @indoorjetpacks something you can do is whenever you see somwhere in your code where your update the gui, and that code has to run in a thread, just make a signal for it, emit that signal instead of updating the GUI inside that function, and have your main thread do the GUI update instead when it receives that signal, ie: by connecting the signal to the function that does the GUI update, while this is not the cleanest thing to do in large applications, it will work just fine for small applications. – Ahmed AEK Oct 28 '22 at 23:22