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 ¶meter) {
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
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).
Adding the codelines
global app
andapp.exit()
to thehandleResults(..)
slot fixes the hanging-problem. But what precisely happens on the background? Are these codelines killing the worker thread? Or the main QApplication thread?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 callingQThread::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
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:Hope it helps.