QThread: Destroyed while thread is still running - PyQT5 Issues



  • I am trying to figure out why this code crashes if I try to run the threads for a second time once they are completed.

    The first time I click "Start 5 Threads" It runs just fine and finishes. But if I click it again. The entire program crashes and I get the QThread: Destroyed while thread is still running Error

    import time
    import sys
    
    from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
    from PyQt5.QtWidgets import QApplication, QPushButton, QTextEdit, QVBoxLayout, QWidget
    
    
    def trap_exc_during_debug(*args):
        # when app raises uncaught exception, print info
        print(args)
    
    
    # install exception hook: without this, uncaught exception would cause application to exit
    sys.excepthook = trap_exc_during_debug
    
    
    class Worker(QObject):
        """
        Must derive from QObject in order to emit signals, connect slots to other signals, and operate in a QThread.
        """
    
        sig_step = pyqtSignal(int, str)  # worker id, step description: emitted every step through work() loop
        sig_done = pyqtSignal(int)  # worker id: emitted at end of work()
        sig_msg = pyqtSignal(str)  # message to be shown to user
    
        def __init__(self, id: int):
            super().__init__()
            self.__id = id
            self.__abort = False
    
        @pyqtSlot()
        def work(self):
            """
            Pretend this worker method does work that takes a long time. During this time, the thread's
            event loop is blocked, except if the application's processEvents() is called: this gives every
            thread (incl. main) a chance to process events, which in this sample means processing signals
            received from GUI (such as abort).
            """
            thread_name = QThread.currentThread().objectName()
            thread_id = int(QThread.currentThreadId())  # cast to int() is necessary
            self.sig_msg.emit('Running worker #{} from thread "{}" (#{})'.format(self.__id, thread_name, thread_id))
    
            for step in range(100):
                time.sleep(0.1)
                self.sig_step.emit(self.__id, 'step ' + str(step))
    
                # check if we need to abort the loop; need to process events to receive signals;
                app.processEvents()  # this could cause change to self.__abort
                if self.__abort:
                    # note that "step" value will not necessarily be same for every thread
                    self.sig_msg.emit('Worker #{} aborting work at step {}'.format(self.__id, step))
                    break
    
            self.sig_done.emit(self.__id)
    
        def abort(self):
            self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id))
            self.__abort = True
    
    
    class MyWidget(QWidget):
        NUM_THREADS = 5
    
        # sig_start = pyqtSignal()  # needed only due to PyCharm debugger bug (!)
        sig_abort_workers = pyqtSignal()
    
        def __init__(self):
            super().__init__()
    
            self.setWindowTitle("Thread Example")
            form_layout = QVBoxLayout()
            self.setLayout(form_layout)
            self.resize(400, 800)
    
            self.button_start_threads = QPushButton()
            self.button_start_threads.clicked.connect(self.start_threads)
            self.button_start_threads.setText("Start {} threads".format(self.NUM_THREADS))
            form_layout.addWidget(self.button_start_threads)
    
            self.button_stop_threads = QPushButton()
            self.button_stop_threads.clicked.connect(self.abort_workers)
            self.button_stop_threads.setText("Stop threads")
            self.button_stop_threads.setDisabled(True)
            form_layout.addWidget(self.button_stop_threads)
    
            self.log = QTextEdit()
            form_layout.addWidget(self.log)
    
            self.progress = QTextEdit()
            form_layout.addWidget(self.progress)
    
            QThread.currentThread().setObjectName('main')  # threads can be named, useful for log output
            self.__workers_done = None
            self.__threads = None
    
        def start_threads(self):
            self.log.append('starting {} threads'.format(self.NUM_THREADS))
            self.button_start_threads.setDisabled(True)
            self.button_stop_threads.setEnabled(True)
    
            self.__workers_done = 0
            self.__threads = []
            for idx in range(self.NUM_THREADS):
                worker = Worker(idx)
                thread = QThread()
                thread.setObjectName('thread_' + str(idx))
                self.__threads.append((thread, worker))  # need to store worker too otherwise will be gc'd
                worker.moveToThread(thread)
    
                # get progress messages from worker:
                worker.sig_step.connect(self.on_worker_step)
                worker.sig_done.connect(self.on_worker_done)
                worker.sig_msg.connect(self.log.append)
    
                # control worker:
                self.sig_abort_workers.connect(worker.abort)
    
                # get read to start worker:
                # self.sig_start.connect(worker.work)  # needed due to PyCharm debugger bug (!); comment out next line
                thread.started.connect(worker.work)
                thread.start()  # this will emit 'started' and start thread's event loop
    
            # self.sig_start.emit()  # needed due to PyCharm debugger bug (!)
    
        @pyqtSlot(int, str)
        def on_worker_step(self, worker_id: int, data: str):
            self.log.append('Worker #{}: {}'.format(worker_id, data))
            self.progress.append('{}: {}'.format(worker_id, data))
    
        @pyqtSlot(int)
        def on_worker_done(self, worker_id):
            self.log.append('worker #{} done'.format(worker_id))
            self.progress.append('-- Worker {} DONE'.format(worker_id))
            self.__workers_done += 1
            if self.__workers_done == self.NUM_THREADS:
                self.log.append('No more workers active')
                self.button_start_threads.setEnabled(True)
                self.button_stop_threads.setDisabled(True)
                # self.__threads = None
    
        @pyqtSlot()
        def abort_workers(self):
            self.sig_abort_workers.emit()
            self.log.append('Asking each worker to abort')
            for thread, worker in self.__threads:  # note nice unpacking by Python, avoids indexing
                thread.quit()  # this will quit **as soon as thread event loop unblocks**
                thread.wait()  # <- so you need to wait for it to *actually* quit
    
            # even though threads have exited, there may still be messages on the main thread's
            # queue (messages that threads emitted before the abort):
            self.log.append('All threads exited')
    
    
    if __name__ == "__main__":
        app = QApplication([])
    
        form = MyWidget()
        form.show()
    
        sys.exit(app.exec_())
    
    

  • Moderators

    @ZSSchulze said in QThread: Destroyed while thread is still running - PyQT5 Issues:

    self.__threads = []

    you call this without stopping/terminating the threads - my guess is that garbage collector deletes the objects (thread + worker). In start_threads you should first make sure all active threads are finished or stop them.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.