18
votes

i read this article How To Really, Truly Use QThreads; The Full Explanation, it says instead of subclass qthread, and reimplement run(), one should use moveToThread to push a QObject onto QThread instance using moveToThread(QThread*)

here is the c++ example, but i don't know how to convert it to python code.

class Worker : public QObject
 {
     Q_OBJECT
     QThread workerThread;

 public slots:
     void doWork(const QString &parameter) {
         // ...
         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, SIGNAL(finished()), worker, SLOT(deleteLater()));
         connect(this, SIGNAL(operate(QString)), worker, SLOT(doWork(QString)));
         connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handleResults(QString)));
         workerThread.start();
     }
     ~Controller() {
         workerThread.quit();
         workerThread.wait();
     }
 public slots:
     void handleResults(const QString &);
 signals:
     void operate(const QString &);
 };



QThread* thread = new QThread;
Worker* worker = new Worker();
worker->moveToThread(thread);
connect(worker, SIGNAL(error(QString)), this, SLOT(errorString(QString)));
connect(thread, SIGNAL(started()), worker, SLOT(process()));
connect(worker, SIGNAL(finished()), thread, SLOT(quit()));
connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
thread->start();

i've been using this method to generate a qthread , but as you can see, it's using the not recommended way. how can i re-write it to use the preferred method ?

class GenericThread(QThread):
    def __init__(self, function, *args, **kwargs):
        QThread.__init__(self)
        # super(GenericThread, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs

    def __del__(self):
        self.wait()

    def run(self, *args):
        self.function(*self.args, **self.kwargs)

edit: two years later ... I tried qris' code, it works and in different thread

import sys
import time
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal, pyqtSlot
import threading


def logthread(caller):
    print('%-25s: %s, %s,' % (caller, threading.current_thread().name,
                              threading.current_thread().ident))


class MyApp(QtGui.QWidget):

    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        self.setGeometry(300, 300, 280, 600)
        self.setWindowTitle('using threads')

        self.layout = QtGui.QVBoxLayout(self)

        self.testButton = QtGui.QPushButton("QThread")
        self.testButton.released.connect(self.test)
        self.listwidget = QtGui.QListWidget(self)

        self.layout.addWidget(self.testButton)
        self.layout.addWidget(self.listwidget)

        self.threadPool = []
        logthread('mainwin.__init__')

    def add(self, text):
        """ Add item to list widget """
        logthread('mainwin.add')
        self.listwidget.addItem(text)
        self.listwidget.sortItems()

    def addBatch(self, text="test", iters=6, delay=0.3):
        """ Add several items to list widget """
        logthread('mainwin.addBatch')
        for i in range(iters):
            time.sleep(delay)  # artificial time delay
            self.add(text+" "+str(i))

    def test(self):
        my_thread = QtCore.QThread()
        my_thread.start()

        # This causes my_worker.run() to eventually execute in my_thread:
        my_worker = GenericWorker(self.addBatch)
        my_worker.moveToThread(my_thread)
        my_worker.start.emit("hello")
        # my_worker.finished.connect(self.xxx)

        self.threadPool.append(my_thread)
        self.my_worker = my_worker


class GenericWorker(QtCore.QObject):

    start = pyqtSignal(str)
    finished = pyqtSignal()

    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()
        logthread('GenericWorker.__init__')
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.start.connect(self.run)

    @pyqtSlot()
    def run(self, *args, **kwargs):
        logthread('GenericWorker.run')
        self.function(*self.args, **self.kwargs)
        self.finished.emit()


# run
app = QtGui.QApplication(sys.argv)
test = MyApp()
test.show()
app.exec_()

the ouput is:

mainwin.__init__         : MainThread, 140221684574016,
GenericWorker.__init__   : MainThread, 140221684574016,
GenericWorker.run        : Dummy-1, 140221265458944,
mainwin.addBatch         : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
mainwin.add              : Dummy-1, 140221265458944,
3
I'm surprised that this question has been unanswered/uncommented for so long. The docs clearly state that using moveToThread(thread) is the preffered way, but yet all example code I've been able to find subclasses QThread.run() and put work there. It would be great if we could see an example or a use pattern.jpcgt
Here is an article that helped a lot when implementing my first thread: mayaposch.wordpress.com/2011/11/01/… . As you said it not a recommended practice to subclass QThread even if there are loads of tutorials doing that.Plouff
Is the final sample in the question correct? mainwin.add is where the code adds the item to the listwidget, but clearly from the printed output, we can see tat mainwin.add is executed in the thread?!?Philip Ridout

3 Answers

14
votes

The default run() implementation in QThread runs an event loop for you, the equivalent of:

class GenericThread(QThread):
    def run(self, *args):
        self.exec_()

The important thing about an event loop is that it allows objects owned by the thread to receive events on their slots, which will be executed in that thread. Those objects are just QObjects, not QThreads.

Important note: the QThread object is not owned by its own thread! It was created on the main thread and lives there. Apart from its run method, all of its code executes in the main thread.

So you should be able to do this:

class GenericWorker(QObject):
    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.start.connect(self.run)

    start = pyqtSignal(str)

    @pyqtSlot
    def run(self, some_string_arg):
        self.function(*self.args, **self.kwargs)

my_thread = QThread()
my_thread.start()

# This causes my_worker.run() to eventually execute in my_thread:
my_worker = GenericWorker(...)
my_worker.moveToThread(my_thread)
my_worker.start.emit("hello")

Also, think carefully about what happens with the result of self.function, which is currently discarded. You could declare another signal on GenericWorker, which receives the result, and have the run() method emit that signal when it's done, passing the result to it.

Once you get the hang of it and realize you don't and shouldn't subclass QThread, life becomes a lot more straightforward and easier. Simply put, never do work in QThread. You should almost never need to override run. For most use cases, setting up proper associations with a QObject to a QThread and using QT's signals/slots creates an extremely powerful way to do multithreaded programming. Just be careful not to let the QObjects you've pushed to your worker threads hang around...

http://ilearnstuff.blogspot.co.uk/2012/09/qthread-best-practices-when-qthread.html

5
votes

I was attempting to use qris's example in my application, but kept having my code run in the my main thread! It is the way the signal that he declared to call run!

Basically, when you connect it in the constructor of the object, the connection will exist between two objects in the main thread - because the QObject's properties belong to the thread that created them. When you move the QObject to your new thread, the connection doesn't move with you. Take away the line that connects your signal to the run function, and connect it after you move the worker to its new thread!

The relevant change from qris's answer:

class GenericWorker(QObject):
    def __init__(self, function, *args, **kwargs):
        super(GenericWorker, self).__init__()

        self.function = function
        self.args = args
        self.kwargs = kwargs

    start = pyqtSignal(str)

    @pyqtSlot
    def run(self, some_string_arg):
        self.function(*self.args, **self.kwargs)

my_thread = QThread()
my_thread.start()

# This causes my_worker.run() to eventually execute in my_thread:
my_worker = GenericWorker(...)
my_worker.moveToThread(my_thread)
my_worker.start.connect(my_worker.run) #  <---- Like this instead 
my_worker.start.emit("hello")
3
votes

I've tried both @qris and @MatthewRunchey approaches.

With the @pyqtSlot decorator Qt checks the "location" of the worker instance when the signal is emitted: even if the connection was made before moveToThread emitting the signal after moveToThread executes the slot in the worker thread.

Without the @pyqtSlot decorator Qt freezes the "location" of the worker instance the moment when the connection was made: if it was before moveToThread, it is bound to the main thread, and the slot code keeps being executed in the main thread even if the signal is emitted after moveToThread call.

Connections made after moveToThread bind the slot to be executed the worker thread in both cases.

Code:

import threading
from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,
                          QThreadPool, pyqtSignal, pyqtSlot)

class Worker(QObject):
    def __init__(self):
        super(Worker, self).__init__()
#        self.call_f1.connect(self.f1)
#        self.call_f2.connect(self.f2)

    call_f1 = pyqtSignal()
    call_f2 = pyqtSignal()

    @pyqtSlot()
    def f1(self):
        print('f1', threading.get_ident())
    
    @pyqtSlot()
    def f2(self):
        print('f2', threading.get_ident())

app = QCoreApplication([])
print('main', threading.get_ident())
my_thread = QThread()
my_thread.start()

my_worker = Worker()
my_worker.call_f1.connect(my_worker.f1)
my_worker.call_f1.emit()
my_worker.moveToThread(my_thread)
my_worker.call_f2.connect(my_worker.f2)
my_worker.call_f1.emit()
my_worker.call_f2.emit()
sys.exit(app.exec_())

With decorator:

main 18708
f1 18708
f1 20156
f2 20156

Without decorator:

main 5520
f1 5520
f1 5520
f2 11472

PS Connecting in the worker __init__ method is obviously equivalent to connecting before moveToThread in the main thread.

(tested under PyQt5, win64).