2

The official documentation on how to instantiate and use a QThread can be found here: http://doc.qt.io/qt-5/qthread.html

The documentation describes two basic approaches: (1) worker-object approach and (2) QThread subclass approach.
I've read in several articles that the second approach is not good, so let's focus on the first one.


EDIT:
@ekhumoro pointed me to the following interesting article: https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html . Apparently both approaches (1) and (2) each have their own merits:

As a rule of thumb:

  • If you do not really need an event loop in the thread, you should subclass.
  • If you need an event loop and handle signals and slots within the thread, you may not need to subclass.
  • As I do need some sort of communication between the QApplication thread and the new QThread (and I believe signal-slot is a good way to communicate), I'll use the worker-object approach.


    1. The worker-object approach in C++

    I've copy-pasted the C++ code of the worker-object approach (from the official Qt5 docs, see http://doc.qt.io/qt-5/qthread.html):

    class Worker : public QObject
    {
        Q_OBJECT
    
    public slots:
        void doWork(const QString &parameter) {
            QString result;
            /* ... here is the expensive or blocking operation ... */
            emit resultReady(result);
        }
    
    signals:
        void resultReady(const QString &result);
    };
    
    class Controller : public QObject
    {
        Q_OBJECT
        QThread workerThread;
    public:
        Controller() {
            Worker *worker = new Worker;
            worker->moveToThread(&workerThread);
            connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
            connect(this, &Controller::operate, worker, &Worker::doWork);
            connect(worker, &Worker::resultReady, this, &Controller::handleResults);
            workerThread.start();
        }
        ~Controller() {
            workerThread.quit();
            workerThread.wait();
        }
    public slots:
        void handleResults(const QString &);
    signals:
        void operate(const QString &);
    };
    

     

    2. The worker-object approach in Python

    I made an effort to translate the given C++ code to Python. If you have Python 3.6 and PyQt5 installed, you can simply copy-paste this code and run it on your machine. It should work.

    import sys
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    
    class Worker(QObject):
    
        resultReady = pyqtSignal(str)
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
        @pyqtSlot(str)
        def doWork(self, param):
            result = "hello world"
            print("foo bar")
            # ...here is the expensive or blocking operation... #
            self.resultReady.emit(result)
    
    
    class Controller(QObject):
    
        operate = pyqtSignal(str)
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            # 1. Create 'workerThread' and 'worker' objects
            # ----------------------------------------------
            self.workerThread = QThread()
            self.worker = Worker()          # <- SEE NOTE(1)
            self.worker.moveToThread(self.workerThread)
    
            # 2. Connect all relevant signals
            # --------------------------------
            self.workerThread.finished.connect(self.worker.deleteLater)
            self.workerThread.finished.connect(lambda: print("workerThread finished."))  # <- SEE NOTE(2)
            self.operate.connect(self.worker.doWork)
            self.worker.resultReady.connect(self.handleResults)
    
            # 3. Start the thread
            # --------------------
            self.workerThread.start()
    
        def __del__(self):
            self.workerThread.quit()
            self.workerThread.wait()
    
        @pyqtSlot(str)
        def handleResults(self, param):
            print(param)
            # One way to end application
            # ---------------------------
            # global app      # <- SEE
            # app.exit()      #     NOTE(3)
    
            # Another way to end application
            # -------------------------------
            self.workerThread.quit()   # <- SEE NOTE(4)
            self.thread().quit()
    
    
    if __name__ == '__main__':
        app = QCoreApplication([])
        controller = Controller()
        controller.operate.emit("foo")      # <- SEE NOTE(5)
        sys.exit(app.exec_())
    

    NOTE (1):
    Initially I had implemented the worker variable as a local variable in the constructor. I was literally translating the C++ sample to Python, and this variable is also a local variable in the C++ sample.
    As you can see in the comment of @pschill, this local variable was garbage collected, and therefore I couldn't get the thread running. After making the change, I get the expected output.

    NOTE (2):
    I've added this line to know precisely when the workerThread finishes.

    NOTE (3):
    Apparently I need to add these two codelines global app and app.exit() to the handleResults(..) slot. Thank you @Matic to point that out!

    NOTE (4):
    I've discovered (through some documentations) this approach to end the application. The first codeline ends the workerThread (by killing its event-loop). The second codeline ends the mainThread (also by killing its event-loop).

    NOTE (5):
    When running the code in a Windows console, nothing happened (it just hanged). On the advice of @pschill (see his comment below), I added this codeline to make sure that the doWork() function gets called.

     

    3. My questions

    1. First and foremost, I would like to know if my translation from C++ to Python is correct. Please show me where I made errors (if you find any).

    2. Adding the codelines global app and app.exit() to the handleResults(..) slot fixes the hanging-problem. But what precisely happens on the background? Are these codelines killing the worker thread? Or the main QApplication thread?

    3. Is there a way to kill the worker thread without killing the main QApplication thread?


    4. Some answers

    1. Still not sure..

     
    2. I believe that app.exit() kills the main thread, which in turn kills the worker thread, because it is of the deamon type. I found out that the worker thread is of deamon type because I inserted the codeline print(threading.current_thread()) in the doWork(..) function. It printed <_DummyThread(Dummy-1, started daemon 9812)>. When a program quits, any daemon threads are killed automatically.

     
    3. Yes, I found a way! The QThread::quit() function is your friend. The official docs say about it:

    void QThread::quit()
    Tells the thread's event loop to exit with return code 0 (success). Equivalent to calling QThread::exit(0).
    This function does nothing if the thread does not have an event loop.
    [http://doc.qt.io/qt-5/qthread.html#quit]

    So my function handleResults(..) now looks like this:

        @pyqtSlot(str)
        def handleResults(self, param):
            print(param)
            self.workerThread.quit()  # Kill the worker thread
            self.thread().quit()      # Kill the main thread
    

    I've checked the kill of the worker thread by inserting this line in the constructor of the Controller(..):

        self.workerThread.finished.connect(lambda: print("workerThread finished."))
    

    I indeed get the line printed out as expected. I've also tried to check the kill of the main thread in a similar way:

        self.thread().finished.connect(lambda: print("mainThread finished."))
    

    Unfortunately this line doesn't print out. Why?


    Hereby I provide my current system settings:
        >  Qt5 (QT_VERSION_STR = 5.10.1)
        >  PyQt5 (PYQT_VERSION_STR = 5.10.1)
        >  Python 3.6.3
        >  Windows 10, 64-bit

    K.Mulier
    • 8,069
    • 15
    • 79
    • 141
    • Currently, noone calls the `doWork` function and that's why you dont see any output. You can, for example, emit the `operate` signal, which is connected to `doWork`. Unrelated to that, Your application hangs because you never stop the thread, so its event loop keeps running. – pschill May 25 '18 at 15:01
    • All the articles claiming that the second approach is somehow "not good" are simply wrong. and should be ignored. The current qt docs correctly include both approaches because they provide valid solutions to slightly different problems. If you want to understand the issues properly, read this: [QThread: You were not doing so wrong](https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html). – ekhumoro May 25 '18 at 15:55
    • As for the code in the qthread docs: it is just skeleton code that is only intended to illustrate how you might use the class. It does not provide any complete examples, so it is not surprising that it doesn't seem to do anything. If you want complete examples, see: [threading examples](https://doc.qt.io/qt-5/examples-threadandconcurrent.html). Several of these have already been ported to pyqt, and can be found [here](https://github.com/baoboa/pyqt5/tree/master/examples/threads). – ekhumoro May 25 '18 at 16:05
    • Hi @ekhumoro , thank you very much! Your comments are very useful (and also the links you provide). I will certainly go through the PyQt examples you linked to. Nevertheless, do you think my "translation" is okay? If you see any errors, feel free to let me know :-) – K.Mulier May 25 '18 at 16:43
    • Hi @pschill , I just added a codeline that should invoke the `doWork()` function. Unfortunately, it doesn't help (see second **EDIT**) – K.Mulier May 25 '18 at 17:43
    • 1
      @K.Mulier The `worker` is just a local variable in `__init__`, it will be deleted the end of the function. You can test this by adding a `__del__` function that calls `print("deleted worker")`. The object stays alive if you make it a member of the controller (use `self.worker` instead of `worker`). Try it and the `hello world` will appear after emitting the `operate` signal :) – pschill May 25 '18 at 18:03
    • Hi @pschill, thank you very much. It works now! I've got two questions: (1) It looks like the `worker` is a local variable in the `Controller()` constructor in the C++ code. That is why I also made it a local variable in my Python translation. Is the C++ code doing something wrong? (2) The output gets printed to the console, but the application still hangs after that (the thread doesn't quit properly). This despite the fact the finished signal is connected (see codeline `self.workerThread.finished.connect(self.worker.deleteLater)`). Why? (3) Why does Qt provide sample code that hangs? – K.Mulier May 25 '18 at 18:15
    • 1
      @K.Mulier To (1): C++ has manual management: An object constructed with `new` lives until it is destroyed with `delete` and in case of Qt, the QObject parent deletes its children. Here, the QThread becomes the worker parent when `moveToThread` is called, so the worker stays alive. Python is different: An object is destroyed when it is not used anymore (reference counting), the Qt parent-child ownership has no effect in Python. You have to keep the object alive by yourself by making it a class member. – pschill May 25 '18 at 20:13
    • 1
      @K.Mulier To (2): As I said, the QThread is never stopped and its event loop keeps running. You can test this by emitting the `operate` signal multiple times: The `doWork` functiom will be called multiple times. You have to stop the thread, for example, by calling `controller.workerThread.quit()` in your `__main__`. – pschill May 25 '18 at 20:20

    1 Answers1

    1

    Your Python example application needs to somehow exit, otherwise it just sits there after the Controller object has been initialized.

    The easiest thing is to change the handleResults function in your example to:

    @pyqtSlot(str)
    def handleResults(self, param):
        print(param)
        global app
        app.exit()
    

    Hope it helps.

    Matic Kukovec
    • 354
    • 2
    • 9
    • 2
      instead of using a global variable you could use `QCoreApplication.instance().quit()` – eyllanesc May 25 '18 at 19:16
    • 1
      Hi @Matic, Thank you very much. You've definitely pushed me in the right direction. But I just found a way to kill the `workerThread` and the `mainThread` one after the other, individually. This gives a fine control over what happens. I've added it at the end of the question :-) – K.Mulier May 27 '18 at 14:47