1

I am encountering this problem QThread: Destroyed while thread is still running when I close the NextWindow QDialog.

The code I wrote is

import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc

from pathlib import Path
from hashlib import sha1


def recursive_hashes(path):
    """Generate name and SHA1 hash of all files under the given path"""
    if path.is_file():
        sha1_obj = sha1()
        try:
            with open(path, 'rb') as handle:
                while True:
                    data = handle.read(8192)
                    if not data:
                        break
                    sha1_obj.update(data)
            sha1_hash = sha1_obj.hexdigest()
        except PermissionError:
            sha1_hash = 'Permission Denied'
        yield (str(path), sha1_hash)
    elif path.is_dir():
        try:
            for child in path.iterdir():
                yield from recursive_hashes(child)
        except PermissionError:
            yield(str(path), 'Permission Denied')
    else:
        yield (str(path), 'Not a file or dir')

class Worker(qtc.QObject):

    hashed = qtc.pyqtSignal(str, str)

    @qtc.pyqtSlot(str)
    def hash_directory(self, root):
        hash_gen = recursive_hashes(Path(root))
        for path, sha1_hash in hash_gen:
            self.hashed.emit(path, sha1_hash)

class NextWindow(qtw.QDialog):

    hash_requested = qtc.pyqtSignal(str)

    def __init__(self):
        """MainWindow constructor."""
        super().__init__()
        layout = qtw.QFormLayout()
        self.setLayout(layout)

        self.file_root = qtw.QLineEdit(returnPressed=self.start_hashing)
        self.go_button = qtw.QPushButton('Start Hashing', clicked=self.start_hashing)
        self.results = qtw.QTableWidget(0, 2)
        self.results.setHorizontalHeaderLabels(['Name', 'SHA1-sum'])
        self.results.horizontalHeader().setSectionResizeMode(qtw.QHeaderView.Stretch)
        self.results.setSizePolicy(qtw.QSizePolicy.Expanding, qtw.QSizePolicy.Expanding)

        layout.addRow(qtw.QLabel('SHA1 Recursive Hasher'))
        layout.addRow('File Root', self.file_root)
        layout.addRow('', self.go_button)
        layout.addRow(self.results)

        # Create a worker object and a thread
        self.worker = Worker()
        self.worker_thread = qtc.QThread(self)
        self.worker.hashed.connect(self.add_hash_to_table)
        self.hash_requested.connect(self.worker.hash_directory)

        # Assign the worker to the thread and start the thread
        self.worker.moveToThread(self.worker_thread)
        self.worker_thread.start()

        # Connect signals & slots AFTER moving the object to the thread


        # End main UI code
        self.show()

    def add_hash_to_table(self, name, sha1_sum):
        """Add the given name and sha1 sum to the table"""

        row = self.results.rowCount()
        self.results.insertRow(row)
        self.results.setItem(row, 0, qtw.QTableWidgetItem(name))
        self.results.setItem(row, 1, qtw.QTableWidgetItem(sha1_sum))
        print(name, sha1_sum)

    def start_hashing(self):
        """Prepare the GUI and emit the hash_requested signal"""

        # Clear the table
        while self.results.rowCount() > 0:
            self.results.removeRow(0)

        # Get the file root and validate it
        file_root = self.file_root.text()
        if not Path(file_root).exists():
            qtw.QMessageBox.critical(self, 'Invalid Path', 'The specified file root does not exist.')
            return

        # Emit the signal
        self.hash_requested.emit(file_root)
        #self.worker.hash_directory(file_root)

class MainWindow(qtw.QDialog):
    def __init__(self):
        super().__init__()
        layout = qtw.QFormLayout()
        
        self.next_button = qtw.QPushButton('Next', clicked=self.next_button)
        layout.addWidget(self.next_button)
        self.setLayout(layout)
    
    def next_button(self):
        dialog = NextWindow()
        dialog.exec_()
        
if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec())

I have checked reference answers in PyQt5 - QThread: Destroyed while thread is still running and QThread: Destroyed while thread is still running.

The answers are basically didnt assign instance to the QThread like

self.thread = QThread(parent=self) While I did so but the issue persisted.

Roy Dai
  • 483
  • 2
  • 5
  • 15
  • Please don't create a new post that is just a copy of [a deleted one](https://stackoverflow.com/q/68078007/2001654). It is considered an abuse, and can also lead to suspension. Edit the existing post, instead, add the required information and notify others about the updates using the comments. – musicamante Jun 22 '21 at 14:29
  • @musicamante thanks for the comment but I have to say this is not just a copy of deleted one. I did post relevent one with same issue but the source code was not in a standard format and I have been severly penalised for those non-reproducable source code. That question has already been closed and I have to deleted the question under pressure. I have actually reproduced the error on another new piece of source code. So I can post this on a new question. – Roy Dai Jun 23 '21 at 05:05

2 Answers2

1

The debug message of QThread: Destroyed while thread is still running can be ubiquitous but the cause of the reason can be not always the same. For this case the thread has not been ended well when you close the dialog even you include the self as

self.worker_thread = qtc.QThread(self)

In this case you should reject the Qthread as mentioned in the question Terminating QThread gracefully on QDialog reject()

So you should add one more line in your code

self.worker_thread.start()
self.rejected.connect(self.thread.terminate)
  • While the concept is correct, the final explanation isn't correct (nor it is the code). The dialog is not rejected, it's destroyed since it goes out of scope and doesn't have a reference, so the problem isn't actually solved. Please be more careful when providing answers that could lead to other issues, and if you're not sure about the results, at least do some tests. – musicamante Jun 22 '21 at 14:39
  • @musicamante thanks for your comment but I dont think you have understood my issue precisely. The error msg appears when the dialog is rejected. There is no evidence why or how it has been destroyed as out of scope or doesnt have a reference. – Roy Dai Jun 23 '21 at 05:13
  • @musicamante hi, I kind of get what you meant. Did you suggest to put `self.dialog = NextWindow() self.dialog.exec_()` for the next widnow? but actully this didnt really matter as I cannot closed the first window once the next window is activated. I updated in the question that the error msg appears when second window is closed. – Roy Dai Jun 23 '21 at 05:50
1

According to the PYQT5 document

When the thread is terminated, all threads waiting for the thread to finish will be woken up.

Warning: This function is dangerous and its use is discouraged. The thread can be terminated at any point in its code path. Threads can be terminated while modifying data. There is no chance for the thread to clean up after itself, unlock any held mutexes, etc. In short, use this function only if absolutely necessary.

So I would suggest to add

self.rejected.connect(self.thread.quit)

or

self.rejected.connect(self.thread.exit)