Why will threading with Qt 5.12 successfully update a QLabel in one function but will crash in another similar function?

120 views Asked by At

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)

1

There are 1 answers

4
Ahmed AEK On

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