1

I have created the following toy class with help from this answer:

class Worker(QtCore.QThread):
    def work(self):
        print("message")

    def __init__(self):
        super(Worker, self).__init__()
        self.timer = QtCore.QTimer() 
        self.timer.moveToThread(self)
        self.timer.timeout.connect(self.work)

    def run(self):
        self.timer.start(1000)
        loop = QtCore.QEventLoop()
        loop.exec_()

How can I start the timer from a new thread when I use QThreadPool?

I need to update the GUI repeatedly at regular intervals but if I add the QTimer in the main thread the whole application feels really sluggish. My understanding is that by including this in a separate thread through QThreadPool it may be a more efficient solution as the new thread can be self deleted automatically once it is done.

However, whenever I change QtCore.QThread to QtCore.QRunnable in the above class and I try to start the thread using the code below I get an error:

self.threadpool = QtCore.QThreadPool()
worker = Worker()
self.threadpool.start(worker)
Giuseppe
  • 143
  • 1
  • 10

1 Answers1

4

If you want to run a task every T seconds with a QThreadPool then it is not necessary for the QTimer to live in another thread, but for the QTimer to start the QRunnable:

import json
import random
import threading
import time

from PyQt5 import QtCore, QtGui, QtWidgets
import sip


class Signaller(QtCore.QObject):
    dataChanged = QtCore.pyqtSignal(object)


class TimerRunnable(QtCore.QRunnable):
    def __init__(self):
        super().__init__()
        self._signaller = Signaller()

    @property
    def signaller(self):
        return self._signaller

    def run(self):
        print("secondary thread:", threading.current_thread())
        time.sleep(0.5)
        r = random.choice(("hello", (1, 2, 3), {"key": "value"}))
        print("send:", r)
        if not sip.isdeleted(self.signaller):
            self.signaller.dataChanged.emit(r)


class Widget(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.text_edit = QtWidgets.QTextEdit()
        self.setCentralWidget(self.text_edit)

        timer = QtCore.QTimer(self, timeout=self.on_timeout, interval=1000)
        timer.start()

    @QtCore.pyqtSlot()
    def on_timeout(self):
        runnable = TimerRunnable()
        runnable.signaller.dataChanged.connect(self.on_data_changed)
        QtCore.QThreadPool.globalInstance().start(runnable)

    @QtCore.pyqtSlot(object)
    def on_data_changed(self, data):
        text = json.dumps(data)
        self.text_edit.append(text)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = Widget()
    w.show()

    print("main thread:", threading.current_thread())

    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I see your point, but if I have multiple timers in the main GUI thread will this not diminish its responsiveness? – Giuseppe May 26 '20 at 16:54
  • 1
    @Giuseppe There is only one QTimer – eyllanesc May 26 '20 at 16:55
  • Yes in this example. But if I have other timers updating different parts of the GUI is this not going to be a problem? – Giuseppe May 26 '20 at 16:56
  • @Giuseppe QTimers do not generate overhead, how many QTImers do you have? – eyllanesc May 26 '20 at 16:57
  • I have four at the moment. My understanding is that they queue events so this can make the GUI less responsive? – Giuseppe May 26 '20 at 16:58
  • 1
    @Giuseppe mmm, plop, 4 QTimers don't generate overhead. Each QTimer uses the eventloop to see if it is time to trigger the signal. In general, the signals use the event loop, they do not queue. – eyllanesc May 26 '20 at 17:00
  • I have two follow-up questions: 1) why do you set a signaller property instead of using the signaller directly i.e. `self._signaller.dataChanged.emit(r)`? and 2) what is `if not sip.isdeleted(self.signaller)` for? – Giuseppe Jul 08 '20 at 08:52