Searene
Searene

Reputation: 27594

QThread.finished signal isn't emitted after worker finishes

I have the following code:

import time

from PyQt5.QtCore import QThread, QObject
from PyQt5.QtWidgets import QWidget, QApplication


class Worker(QObject):

    def run(self):
        time.sleep(1)
        print("Worker is finished")


class MainWindow(QWidget):

    def __init__(self):
        super().__init__()
        self.show()

        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.thread.finished.connect(self.on_thread_finished)
        self.thread.start()

    def on_thread_finished(self):
        print("Thread finished")
        self.thread.deleteLater()


if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    app.exec_()

Run it, it shows the window, prints Worker is finished, nothing more. That's weird. IMHO when the worker is finished, the thread should be finished too, which means the on_thread_finished method should be called and Thread finished should be printed. But it wasn't. Why?

Upvotes: 3

Views: 3272

Answers (2)

ekhumoro
ekhumoro

Reputation: 120638

When you use moveToThread instead of reimplementing QThread.run, the thread will start its own event-loop which will wait until exit/quit is explicitly called. The finished signal is only emitted after the event-loop has stopped (and/or once run returns). The usual way to handle this scenario is to emit a custom signal from the worker, and connect it to the thread's quit slot. (NB: cross-thread signal-slot connections are guaranteed to be thread-safe).

So your example will work as expected with the following changes:

class Worker(QObject):
    finished = QtCore.pyqtSignal()

    def run(self):
        time.sleep(1)
        print("Worker is finished")
        self.finished.emit()


class MainWindow(QWidget):    
    def __init__(self):
        ...
        self.worker.moveToThread(self.thread)
        self.worker.finished.connect(self.thread.quit)

Upvotes: 3

G.M.
G.M.

Reputation: 12899

when the worker is finished, the thread should be finished too

That's not how it works. Your Worker::run method is being invoked as a slot via the usual signal/slot mechanism after which the QThread will continue to process events as normal.

If you want to terminate the QThread when Worker::run has completed you need to tell it to do so explicitly...

import time

from PyQt5.QtCore import Qt, QThread, QObject
from PyQt5.QtWidgets import QWidget, QApplication


class Worker(QObject):

    # Constructor accepts the QThread as a parameter and stashes it
    # for later use.
    def __init__(self, thread):
        super(Worker, self).__init__()
        self.m_thread = thread
    def run(self):
        time.sleep(1)
        print("Worker is finished")

        # We're done so ask the `QThread` to terminate.
        if self.m_thread:
            self.m_thread.quit()


class MainWindow(QWidget):

    def __init__(self):
        super().__init__()
        self.show()

        self.thread = QThread()

        # Pass the QThread to the Worker's ctor.
        self.worker = Worker(self.thread)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.thread.finished.connect(self.on_thread_finished)
        self.thread.start()

    def on_thread_finished(self):
        print("Thread finished")
        self.thread.deleteLater()


if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    app.exec_()

Very 'inelegant' but it conveys the idea.

A simpler alternative that springs to mind would be to simply make use of QThread::currentThread(), in which case your Worker::run method becomes...

class Worker(QObject):
    def run(self):
        time.sleep(1)
        print("Worker is finished")

        # We're done so ask the `QThread` to terminate.
        QThread.currentThread().quit()

Upvotes: 2

Related Questions