Search code examples
pythonpysideqthread

Multiple QThread keep crashing PySide


I am trying to implement a program with multiple threads. In the __init__ of the main window, the threads are created. The GUI starts up, while the threads get run in the background. The problem is that it keeps crashing. But if I add/uncomment a line with a print statement, the program works fine.

class TestThreadingWin(QtGui.QMainWindow):

    def __init__(self,  parent=rsui.getMayaMainWindow()):
        ''' Constructor '''
        super(TestThreadingWin, self).__init__(parent)

        self.setWindowTitle('Test Threading')

        self.centralWidget = QtGui.QPlainTextEdit()
        self.setCentralWidget(self.centralWidget)

        self.centralWidget.appendPlainText("test")

        numThreads = 7
        self._threads = []

        for i in range(numThreads):
            #print self._threads  # <-- Uncomment this line, and it works?!
            testThread = QtCore.QThread()
            # Keep a reference to the thread object or it will be deleted when
            # it goes out of scope, even if it has not finished processing.
            self._threads.append(testThread)
            worker = TestThreadWorker(i)
            worker.moveToThread(testThread)

            worker.finishedProcessing.connect(self.updateStuff)
            worker.finishedProcessing.connect(testThread.quit)
            testThread.started.connect(worker.doStuff)
            testThread.finished.connect(self.deleteThread)

            testThread.start()

            QtCore.QCoreApplication.processEvents()

        print 'done creating all threads'

    def deleteThread(self):
        """ Destroy the thread object and remove the reference to it from the 
        self._threads list. """
        print 'delete thread'
        threadToDelete = self.sender()
        threadIndex = self._threads.index(threadToDelete)
        del self._threads[threadIndex]
        threadToDelete.deleteLater()

    def updateStuff(self, message):
        self.centralWidget.appendPlainText(message)

class TestThreadWorker(QtCore.QObject):
    finishedProcessing = QtCore.Signal(str)

    def __init__(self, num):
        super(TestThreadWorker, self).__init__()
        self._num = num

    def doStuff(self):
        time.sleep(1)
        choiceList = ['cat', 'bat', 'hat', 'tissue', 'paper', 'qwerty', 'mouse']
        stuff = random.choice(choiceList)
        stuff2 = '{0} {1}'.format(self._num, stuff)
        self.finishedProcessing.emit(stuff2)

def openThreadingWin():
    '''This ensures that only one instance of the UI is open at a time.'''
    global testingThreadingWin
    try:
        testingThreadingWin.close()
        testingThreadingWin.deleteLater()
    except: pass
    testingThreadingWin = TestThreadingWin()
    testingThreadingWin.show()

It is weird that a print statement would make it stop crashing. What am I overlooking?


Solution

  • I finally got it working by using QThreadPool. It manages the threads for me, so I don't have to worry about trying to access something that was already destroyed. Key differences:

    • The worker class now inherits from both QtCore.QObject and QtCore.QRunnable. (It has to inherit from QObject in order to emit a signal.) The __init__ function of both parent classes must be called, or the program will crash.
    • No more code to set up connections to ensure the thread will be destroyed when done.
    • No more code to keep references to threads or delete those references when the thread is destroyed.

    Here is the new code:

    class TestThreadPoolWin(QtGui.QMainWindow):
        def __init__(self,  parent=rsui.getMayaMainWindow()):
            ''' Constructor '''
            super(TestThreadPoolWin, self).__init__(parent)
    
            self.setWindowTitle('Test Threading')
    
            self.centralWidget = QtGui.QPlainTextEdit()
            self.setCentralWidget(self.centralWidget)
    
            self.centralWidget.appendPlainText("test")
    
            numThreads = 7
            threadPool = QtCore.QThreadPool.globalInstance()
            for i in range(numThreads):
                runnable = TestRunnableWorker(i)
                runnable.finishedProcessing.connect(self.updateStuff)
                threadPool.start(runnable)
    
            print 'done creating all threads'
    
        def updateStuff(self, message):
            self.centralWidget.appendPlainText(message)
    
    class TestRunnableWorker(QtCore.QObject, QtCore.QRunnable):
        finishedProcessing = QtCore.Signal(str)
    
        def __init__(self, num, parent=None):
            # Be sure to run the __init__ of both parent classes, or else you will
            # get a crash.
            QtCore.QObject.__init__(self, parent)
            QtCore.QRunnable.__init__(self)
    
            self._num = num
    
        def run(self):
            time.sleep(1)
            choiceList = ['cat', 'bat', 'hat', 'tissue', 'paper', 'qwerty', 'mouse']
            stuff = random.choice(choiceList)
            stuff2 = '{0} {1}'.format(self._num, stuff)
            self.finishedProcessing.emit(stuff2)
    
    def openThreadPoolWin():
        '''This ensures that only one instance of the UI is open at a time.'''
    
        global testingThreadPoolWin
        try:
            testingThreadPoolWin.close()
            testingThreadPoolWin.deleteLater()
        except: pass
        testingThreadPoolWin = TestThreadPoolWin()
        testingThreadPoolWin.show()