Example of the Right Way to Use Qthread in Pyqt

Example of the right way to use QThread in PyQt?

Here is a working example of a separate worker thread which can send and receive signals to allow it to communicate with a GUI.

I made two simple buttons, one which starts a long calculation in a separate thread, and one which immediately terminates the calculation and resets the worker thread.

Forcibly terminating a thread as is done here is not generally the best way to do things, but there are situations in which always gracefully exiting is not an option.

from PyQt4 import QtGui, QtCore
import sys
import random

class Example(QtCore.QObject):

signalStatus = QtCore.pyqtSignal(str)

def __init__(self, parent=None):
super(self.__class__, self).__init__(parent)

# Create a gui object.
self.gui = Window()

# Create a new worker thread.
self.createWorkerThread()

# Make any cross object connections.
self._connectSignals()

self.gui.show()

def _connectSignals(self):
self.gui.button_cancel.clicked.connect(self.forceWorkerReset)
self.signalStatus.connect(self.gui.updateStatus)
self.parent().aboutToQuit.connect(self.forceWorkerQuit)

def createWorkerThread(self):

# Setup the worker object and the worker_thread.
self.worker = WorkerObject()
self.worker_thread = QtCore.QThread()
self.worker.moveToThread(self.worker_thread)
self.worker_thread.start()

# Connect any worker signals
self.worker.signalStatus.connect(self.gui.updateStatus)
self.gui.button_start.clicked.connect(self.worker.startWork)

def forceWorkerReset(self):
if self.worker_thread.isRunning():
print('Terminating thread.')
self.worker_thread.terminate()

print('Waiting for thread termination.')
self.worker_thread.wait()

self.signalStatus.emit('Idle.')

print('building new working object.')
self.createWorkerThread()

def forceWorkerQuit(self):
if self.worker_thread.isRunning():
self.worker_thread.terminate()
self.worker_thread.wait()

class WorkerObject(QtCore.QObject):

signalStatus = QtCore.pyqtSignal(str)

def __init__(self, parent=None):
super(self.__class__, self).__init__(parent)

@QtCore.pyqtSlot()
def startWork(self):
for ii in range(7):
number = random.randint(0,5000**ii)
self.signalStatus.emit('Iteration: {}, Factoring: {}'.format(ii, number))
factors = self.primeFactors(number)
print('Number: ', number, 'Factors: ', factors)
self.signalStatus.emit('Idle.')

def primeFactors(self, n):
i = 2
factors = []
while i * i <= n:
if n % i:
i += 1
else:
n //= i
factors.append(i)
if n > 1:
factors.append(n)
return factors

class Window(QtGui.QWidget):

def __init__(self):
QtGui.QWidget.__init__(self)
self.button_start = QtGui.QPushButton('Start', self)
self.button_cancel = QtGui.QPushButton('Cancel', self)
self.label_status = QtGui.QLabel('', self)

layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button_start)
layout.addWidget(self.button_cancel)
layout.addWidget(self.label_status)

self.setFixedSize(400, 200)

@QtCore.pyqtSlot(str)
def updateStatus(self, status):
self.label_status.setText(status)

if __name__=='__main__':
app = QtGui.QApplication(sys.argv)
example = Example(app)
sys.exit(app.exec_())

How to use QThread correctly in pyqt with moveToThread()?

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 [docs]:

It is important to remember that a QThread instance lives in the old thread that instantiated it, not in the new thread that calls run(). This means that all of QThread's queued slots and invoked methods will execute in the old thread [e.g. 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

Background thread with QThread in PyQt

I created a little example that shows 3 different and simple ways of dealing with threads. I hope it will help you find the right approach to your problem.

import sys
import time

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

# Subclassing QThread
# http://qt-project.org/doc/latest/qthread.html
class AThread(QThread):

def run(self):
count = 0
while count < 5:
time.sleep(1)
print("A Increasing")
count += 1

# Subclassing QObject and using moveToThread
# http://blog.qt.digia.com/blog/2007/07/05/qthreads-no-longer-abstract
class SomeObject(QObject):

finished = pyqtSignal()

def long_running(self):
count = 0
while count < 5:
time.sleep(1)
print("B Increasing")
count += 1
self.finished.emit()

# Using a QRunnable
# http://qt-project.org/doc/latest/qthreadpool.html
# Note that a QRunnable isn't a subclass of QObject and therefore does
# not provide signals and slots.
class Runnable(QRunnable):

def run(self):
count = 0
app = QCoreApplication.instance()
while count < 5:
print("C Increasing")
time.sleep(1)
count += 1
app.quit()

def using_q_thread():
app = QCoreApplication([])
thread = AThread()
thread.finished.connect(app.exit)
thread.start()
sys.exit(app.exec_())

def using_move_to_thread():
app = QCoreApplication([])
objThread = QThread()
obj = SomeObject()
obj.moveToThread(objThread)
obj.finished.connect(objThread.quit)
objThread.started.connect(obj.long_running)
objThread.finished.connect(app.exit)
objThread.start()
sys.exit(app.exec_())

def using_q_runnable():
app = QCoreApplication([])
runnable = Runnable()
QThreadPool.globalInstance().start(runnable)
sys.exit(app.exec_())

if __name__ == "__main__":
#using_q_thread()
#using_move_to_thread()
using_q_runnable()

Threading in a PyQt application: Use Qt threads or Python threads?

This was discussed not too long ago in PyQt mailing list. Quoting Giovanni Bajo's comments on the subject:

It's mostly the same. The main difference is that QThreads are better
integrated with Qt (asynchrnous signals/slots, event loop, etc.).
Also, you can't use Qt from a Python thread (you can't for instance
post event to the main thread through QApplication.postEvent): you
need a QThread for that to work.

A general rule of thumb might be to use QThreads if you're going to interact somehow with Qt, and use Python threads otherwise.

And some earlier comment on this subject from PyQt's author: "they are both wrappers around the same native thread implementations". And both implementations use GIL in the same way.

Simplest way for PyQT Threading

I believe the best approach is using the signal/slot mechanism. Here is an example. (Note: see the EDIT below that points out a possible weakness in my approach).

from PyQt4 import QtGui
from PyQt4 import QtCore

# Create the class 'Communicate'. The instance
# from this class shall be used later on for the
# signal/slot mechanism.

class Communicate(QtCore.QObject):
myGUI_signal = QtCore.pyqtSignal(str)

''' End class '''

# Define the function 'myThread'. This function is the so-called
# 'target function' when you create and start your new Thread.
# In other words, this is the function that will run in your new thread.
# 'myThread' expects one argument: the callback function name. That should
# be a function inside your GUI.

def myThread(callbackFunc):
# Setup the signal-slot mechanism.
mySrc = Communicate()
mySrc.myGUI_signal.connect(callbackFunc)

# Endless loop. You typically want the thread
# to run forever.
while(True):
# Do something useful here.
msgForGui = 'This is a message to send to the GUI'
mySrc.myGUI_signal.emit(msgForGui)
# So now the 'callbackFunc' is called, and is fed with 'msgForGui'
# as parameter. That is what you want. You just sent a message to
# your GUI application! - Note: I suppose here that 'callbackFunc'
# is one of the functions in your GUI.
# This procedure is thread safe.

''' End while '''

''' End myThread '''

In your GUI application code, you should create the new Thread, give it the right callback function, and make it run.

from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import os

# This is the main window from my GUI

class CustomMainWindow(QtGui.QMainWindow):

def __init__(self):
super(CustomMainWindow, self).__init__()
self.setGeometry(300, 300, 2500, 1500)
self.setWindowTitle("my first window")
# ...
self.startTheThread()

''''''

def theCallbackFunc(self, msg):
print('the thread has sent this message to the GUI:')
print(msg)
print('---------')

''''''

def startTheThread(self):
# Create the new thread. The target function is 'myThread'. The
# function we created in the beginning.
t = threading.Thread(name = 'myThread', target = myThread, args = (self.theCallbackFunc))
t.start()

''''''

''' End CustomMainWindow '''

# This is the startup code.

if __name__== '__main__':
app = QtGui.QApplication(sys.argv)
QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())

''' End Main '''

EDIT

Mr. three_pineapples and Mr. Brendan Abel pointed out a weakness in my approach. Indeed, the approach works fine for this particular case, because you generate / emit the signal directly. When you deal with built-in Qt signals on buttons and widgets, you should take another approach (as specified in the answer of Mr. Brendan Abel).

Mr. three_pineapples adviced me to start a new topic in StackOverflow to make a comparison between the several approaches of thread-safe communication with a GUI. I will dig into the matter, and do that tomorrow :-)

How to use a Qthread to update a Matplotlib figure with PyQt?

The first problem is that you lose the reference to thread once it's started. To keep a reference use a class variable, i.e. self.thread instead of thread.

Next, the thread has to be started before doing anything. So you need to put self.thread.start() in front of the signal emission.

Now, it would work already, but a next problem occurs once you want to start a new thread. So, you need to first kill the old one. Since the old Plotter would then be homeless, a solution is to create a new Plotter as well as a new thread each time you want to plot. This is the way the solution below works.

Alternatively, you could also always use the same plotter and thread. The only thing to remember is that there is always exactly one worker (plotter) and one thread, if you delete one of them, the other is sad.

In order to test it, I needed to change some small things, like using PyQt4 instead of 5 and replace the data generation.
Here is the working code.

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from matplotlib.axes._subplots import Axes
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import sys
from datetime import datetime, timedelta
import numpy as np

class MyMplCanvas(FigureCanvas):
"""Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""
send_fig = pyqtSignal(Axes, str, name="send_fig")

def __init__(self, parent=None):
self.fig = Figure()
self.axes = self.fig.add_subplot(111)

# We want the axes cleared every time plot() is called
self.axes.hold(False)

FigureCanvas.__init__(self, self.fig)
self.setParent(parent)

FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)

def update_plot(self, axes):
self.axes = axes
self.draw()

class MainWindow(QMainWindow):
send_fig = pyqtSignal(Axes, str, name="send_fig")

def __init__(self):
super(MainWindow, self).__init__()

self.main_widget = QWidget(self)
self.myplot = MyMplCanvas(self.main_widget)
self.editor = QLineEdit()
self.display = QLabel("Vide")

self.layout = QGridLayout(self.main_widget)
self.layout.addWidget(self.editor)
self.layout.addWidget(self.display)
self.layout.addWidget(self.myplot)

self.main_widget.setFocus()
self.setCentralWidget(self.main_widget)

self.move(500, 500)
self.show()

self.editor.returnPressed.connect(self.updatePlot)

# plotter and thread are none at the beginning
self.plotter = None
self.thread = None

def updatePlot(self):
ticker = self.editor.text()
self.editor.clear()
self.display.setText(ticker)

# if there is already a thread running, kill it first
if self.thread != None and self.thread.isRunning():
self.thread.terminate()

# initialize plotter and thread
# since each plotter needs its own thread
self.plotter = Plotter()
self.thread = QThread()
# connect signals
self.send_fig.connect(self.plotter.replot)
self.plotter.return_fig.connect(self.myplot.update_plot)
#move to thread and start
self.plotter.moveToThread(self.thread)
self.thread.start()
# start the plotting
self.send_fig.emit(self.myplot.axes, ticker)

class Plotter(QObject):
return_fig = pyqtSignal(Axes)

@pyqtSlot(Axes, str)
def replot(self, axes, ticker): # A slot takes no params
print(ticker)
d = datetime.today() - timedelta(weeks=52) # data from 1week ago
# do some random task
data = np.random.rand(10000,10000)
axes.plot(data.mean(axis=1))
self.return_fig.emit(axes)

if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
sys.exit(app.exec_())

Here is a solution for the second option mentionned, i.e. create a single worker and a thread and use those throughout the program's runtime.

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import sys
import numpy as np

class MyMplCanvas(FigureCanvas):

def __init__(self, parent=None):
self.fig = Figure()
self.axes = self.fig.add_subplot(111)
# plot empty line
self.line, = self.axes.plot([],[], color="orange")

FigureCanvas.__init__(self, self.fig)
self.setParent(parent)

FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)

class MainWindow(QMainWindow):
send_fig = pyqtSignal(str)

def __init__(self):
super(MainWindow, self).__init__()

self.main_widget = QWidget(self)
self.myplot = MyMplCanvas(self.main_widget)
self.editor = QLineEdit()
self.display = QLabel("Vide")

self.layout = QGridLayout(self.main_widget)
self.layout.addWidget(self.editor)
self.layout.addWidget(self.display)
self.layout.addWidget(self.myplot)

self.main_widget.setFocus()
self.setCentralWidget(self.main_widget)
self.show()

# plotter and thread are none at the beginning
self.plotter = Plotter()
self.thread = QThread()

# connect signals
self.editor.returnPressed.connect(self.start_update)
self.send_fig.connect(self.plotter.replot)
self.plotter.return_fig.connect(self.plot)
#move to thread and start
self.plotter.moveToThread(self.thread)
self.thread.start()

def start_update(self):
ticker = self.editor.text()
self.editor.clear()
self.display.setText(ticker)
# start the plotting
self.send_fig.emit(ticker)

# Slot receives data and plots it
def plot(self, data):
# plot data
self.myplot.line.set_data([np.arange(len(data)), data])
# adjust axes
self.myplot.axes.set_xlim([0,len(data) ])
self.myplot.axes.set_ylim([ data.min(),data.max() ])
self.myplot.draw()

class Plotter(QObject):
return_fig = pyqtSignal(object)

@pyqtSlot(str)
def replot(self, ticker):
print(ticker)
# do some random task
data = np.random.rand(10000,10000)
data = data.mean(axis=1)
self.return_fig.emit(data)

if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
sys.exit(app.exec_())

How to use QThread() within class of QWidget function?

The main problem in your code is the while True since this in itself blocks the eventloop so you have 2 options:

Threads

NOTE: If the get_live_data() task is time consuming then you should use threads.

You have to be clear about the following:

  • Threads should only be used as a last option.
  • Threads should only execute tasks that consume a lot of time since if they are executed in the main thread, the Qt eventloop will be blocked and consequently the GUI will freeze.
  • The GUI should not be modified directly in the threads, instead you should send the information through signals so that it is updated in the main thread.

Considering the above, the solution is:

class Worker(QObject):
dataChanged = pyqtSignal(dict)

def task(self):
while True:
market = utility.get_live_data()
self.dataChanged.emit(market)

class mainUI(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent, Qt.WindowStaysOnTopHint)

self.setMinimumSize(1000, 500)

self.table_live = QTableWidget()
self.table_live.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table_live.setColumnCount(6)
self.table_live.setHorizontalHeaderLabels(
["Name", "Quantity", "Discount Price", "Market Price", "Change", "P/L"]
)

layout = QHBoxLayout(self)
layout.addWidget(self.table_live)

self.worker = Worker()
thread = QThread(self)
self.worker.moveToThread(thread)
self.worker.dataChanged.connect(self.update_table_live)
thread.started.connect(self.worker.task)
thread.start()

def update_table_live(self, market):
for name, product_data in market.items():
row_count = self.table_live.rowCount()
self.table_live.insertRow(row_count)

item_name = QTableWidgetItem(name)
item_quantity = QTableWidgetItem(product_data["quantity"])
item_discount = QTableWidgetItem(product_data["discount"])
item_current = QTableWidgetItem(product_data["price"])
item_change = QTableWidgetItem(product_data["change"])

item_pl = QTableWidgetItem(product_data["p_l"])

self.table_live.setItem(row_count, 0, item_name)
self.table_live.setItem(row_count, 1, item_quantity)
self.table_live.setItem(row_count, 2, item_discount)
self.table_live.setItem(row_count, 3, item_current)
self.table_live.setItem(row_count, 4, item_change)
self.table_live.setItem(row_count, 5, item_pl)

QTimer

NOTE: If get_live_data is not very time consuming then it is better to use QTimer to avoid blocking loop.

class mainUI(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent, Qt.WindowStaysOnTopHint)

self.setMinimumSize(1000, 500)

self.table_live = QTableWidget()
self.table_live.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table_live.setColumnCount(6)
self.table_live.setHorizontalHeaderLabels(
["Name", "Quantity", "Discount Price", "Market Price", "Change", "P/L"]
)

layout = QHBoxLayout(self)
layout.addWidget(self.table_live)

self.timer = QTimer(interval=100, timeout=self.handle_timeout)
self.timer.start()

def handle_timeout(self):
market = utility.get_live_data()
self.update_table_live(market)

def update_table_live(self, market):
for name, product_data in market.items():
row_count = self.table_live.rowCount()
self.table_live.insertRow(row_count)

item_name = QTableWidgetItem(name)
item_quantity = QTableWidgetItem(product_data["quantity"])
item_discount = QTableWidgetItem(product_data["discount"])
item_current = QTableWidgetItem(product_data["price"])
item_change = QTableWidgetItem(product_data["change"])

item_pl = QTableWidgetItem(product_data["p_l"])

self.table_live.setItem(row_count, 0, item_name)
self.table_live.setItem(row_count, 1, item_quantity)
self.table_live.setItem(row_count, 2, item_discount)
self.table_live.setItem(row_count, 3, item_current)
self.table_live.setItem(row_count, 4, item_change)
self.table_live.setItem(row_count, 5, item_pl)


Related Topics



Leave a reply



Submit