Search code examples
pythonpython-3.xpyqtpyqt5qthread

Using QThread and pyqtSignal, why does the process which is in a different thread freeze the GUI?


I have an application with a thread performing a lengthy task. I trigger the lengthy task using a QPushButton which emits a signal. However, clicking the button makes the GUI unresponsive for the length of the process even though that process is in another thread. Here is a small program replicating this behavior:

import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
from PyQt5.QtCore import pyqtSignal, QThread, Qt


class WriteThread(QThread):
    write_signal = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.write_signal.connect(self.worker, Qt.QueuedConnection)

    def worker(self):
        print("Before sleep")
        time.sleep(2)
        print("After sleep")
        return True


class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.CustomEvent = None
        self.write_thread = None
        self.init_ui()

    def init_ui(self):
        self.write_thread = WriteThread()

        redb = QPushButton('Red', self)
        redb.move(10, 10)

        blueb = QPushButton('Blue', self)
        blueb.move(10, 50)
        blueb.clicked.connect(self.print_method)

        redb.clicked.connect(self.send_event)

        self.write_thread.start()

        self.setGeometry(300, 300, 300, 250)
        self.setWindowTitle('Toggle button')
        self.show()

    def send_event(self):
        print("\tSending signal")
        self.write_thread.write_signal.emit()
        print("\tFinished sending signal")

    def print_method(self):
        print("Not frozen")


def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

The time.sleep(2) simulates the lengthy process. My question really is: why does the process which is in a different thread freeze the GUI?


Solution

  • Explanation:

    You have to understand that QThread is not a Qt-thread but a handler for native OS threads, similar to threading.Thread.

    As the docs points out:

    Unlike queued slots or invoked methods, methods called directly on the QThread object will execute in the thread that calls the method. When subclassing QThread, keep in mind that the constructor executes in the old thread while run() executes in the new thread. If a member variable is accessed from both functions, then the variable is accessed from two different threads. Check that it is safe to do so.

    (emphasis mine)

    That is, only the run() method is executed on the new thread, so "worker" is executed on the thread that belongs to the QThread, which in this case is the main thread.

    Solution:

    If you want the "work" method to be executed then the class object must live in the secondary thread so for that it is enough that WriteThread is a QObject (it does not need to be QThread), and move it to another thread:

    import sys
    import time
    from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
    from PyQt5.QtCore import pyqtSignal, pyqtSlot, QThread, Qt, QObject
    
    
    class WriteObject(QObject):
        write_signal = pyqtSignal()
    
        def __init__(self):
            super().__init__()
            self.write_signal.connect(self.worker, Qt.QueuedConnection)
    
        @pyqtSlot()
        def worker(self):
            print("Before sleep")
            time.sleep(2)
            print("After sleep")
            return True
    
    
    class Example(QWidget):
        def __init__(self):
            super().__init__()
            self.CustomEvent = None
            self.write_thread = None
            self.init_ui()
    
        def init_ui(self):
            self.write_object = WriteObject()
            self.write_thread = QThread(self)
            self.write_thread.start()
            self.write_object.moveToThread(self.write_thread)
    
            redb = QPushButton("Red", self)
            redb.move(10, 10)
    
            blueb = QPushButton("Blue", self)
            blueb.move(10, 50)
            blueb.clicked.connect(self.print_method)
    
            redb.clicked.connect(self.send_event)
    
            self.setGeometry(300, 300, 300, 250)
            self.setWindowTitle("Toggle button")
            self.show()
    
        def send_event(self):
            print("\tSending signal")
            self.write_object.write_signal.emit()
            print("\tFinished sending signal")
    
        def print_method(self):
            print("Not frozen")
    
    
    def main():
        app = QApplication(sys.argv)
        ex = Example()
        ret = app.exec_()
        ex.write_thread.quit()
        ex.write_thread.wait()
        sys.exit(ret)
    
    
    if __name__ == "__main__":
        main()