Background Thread With Qthread in Pyqt

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()

Running multiple background threads using QThread in PyQt5

"obj" lives in the same thread so when invoking the slots they will execute the same thread so the first function it executes will block the other. The solution is to create 2 workers that live in different threads

class Worker(QObject):
finished = pyqtSignal()
intReady = pyqtSignal(int)

@pyqtSlot()
def procCounter(self):
pass

class Worker1(Worker):
@pyqtSlot()
def procCounter(self):
for i in range(1, 100):
time.sleep(.5)
self.intReady.emit(i)
self.finished.emit()

class Worker2(QObject):
@pyqtSlot(int)
def procCounter(self):
for i in range(1000):
time.sleep(.2)
self.intReady.emit(i)
self.finished.emit()
self.obj1 = worker.Worker1()
self.thread1 = QThread()
self.obj1.moveToThread(self.thread1)
self.obj1.intReady.connect(self.onIntReady)
self.thread1.started.connect(self.obj.procCounter)

self.obj2 = worker.Worker2()
self.thread2 = QThread()
self.obj2.moveToThread(self.thread2)
self.obj2.intReady.connect(self.onIntReady2)
self.thread2.started.connect(self.obj2.procCounter)

self.thread1.start()
self.thread2.start()

Is a QTimer in a background thread updated even if the main thread is busy?

In short words, yes, QTimer will update on the other thread, granted that you take care of the thread's lifespan yourself and quit it only at the most appropriate times. Also be careful about the QTimer's reference, so it is not collected by the python's garbage collector (you'll need to stop the QTimer at one point).

Now, with a bit of code to illustrate a more practical example, and so you can test it yourself:

from PySide2.QtWidgets import QApplication, QMainWindow
from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
from PySide2.QtCore import QObject, QThread, QTimer, Signal

class TimerWorker(QObject):
timeout = Signal(int)

def __init__(self):
QObject.__init__(self)
self.count = 0

def run(self):
self.timer = QTimer()
self.timer.setInterval(250)
self.timer.timeout.connect(self.process)
self.timer.start()

def finished(self):
self.timer.stop()

def process(self):
self.count += 1
self.timeout.emit(self.count)

class Window(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)

# Widgets
self.scene = QWidget()
self.label = QLabel("Count: 0")
self.busyBt = QPushButton("Sleep")
self.busyBt.clicked.connect(self.becomeBusy)

# Scene
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.busyBt)
self.scene.setLayout(layout)
self.setCentralWidget(self.scene)

# Threads
self.thread = QThread()
self.worker = TimerWorker()
self.worker.moveToThread(self.thread)
self.worker.timeout.connect(self.processCount)
self.thread.started.connect(self.worker.run)
self.thread.finished.connect(self.worker.finished)
self.thread.start()

def becomeBusy(self):
timeout = 5
print('Main Thread: Sleeping for %d seconds...' % (timeout))
QThread.sleep(timeout)
print('Main Thread: Awaken')

def processCount(self, count):
self.label.setText("Count: %d" % (count))

def closeEvent(self, evt):
self.thread.quit()
self.thread.wait()

if __name__ == '__main__':
app = QApplication()
win = Window()
win.show()
app.exec_()

In this example, we start a QThread which executes TimerWorker.run() on the other thread (using the moveToThread method). At each 0.25 seconds, it emits a signal that is scheduled on the main thread, to update the QLabel text.

At the same time, the user can click a QPushButton to stop the main thread for 5 seconds. During that time, the application will freeze.


By executing the script and clicking on the QPushButton to freeze the main thread, the QTimer keeps running and emitting the signal from the other thread. But as the main thread is sleeping (or busy), the signal is never processed. The signal will only be processed after the main thread awakes.

So, if you need to keep the GUI always responsive for the Client (and always processing emitted signals), its recomended that you do all the heavy work on the QThread side, to not keep the application frozen for a time.

Cancel background task (terminate QThread) in PyQt

It's a very common mistake to try to kill a QThread in the way you're suggesting. This seems to be due to a failure to realise that it's the long-running task that needs to be stopped, rather than the thread itself.

The task was moved to the worker thread because it was blocking the main/GUI thread. But that situation doesn't really change once the task is moved. It will block the worker thread in exactly the same way that it was blocking the main thread. For the thread to finish, the task itself either has to complete normally, or be programmatically halted in some way. That is, something must be done to allow the thread's run() method to exit normally (which often entails breaking out of a blocking loop).

A common way to cancel a long-running task is via a simple stop-flag:

class Thread(QThread):
def stop(self):
self._flag = False

def run(self):
self._flag = True
for item in get_items():
process_item(item)
if not self._flag:
break
self._flag = False

How to create threads in Python for use with PyQt

Classes you need to love, the sooner the better!
One option for what you want may look like this:

import sys
import threading
from PyQt5 import QtWidgets, QtCore # ,uic

def thread(my_func):
""" Runs a function in a separate thread. """
def wrapper(*args, **kwargs):
my_thread = threading.Thread(target=my_func, args=args, kwargs=kwargs)
my_thread.start()
return wrapper

@thread
def processing(signal):
""" Emulates the processing of some data. """
ind = 1
for i in range (1,10001): # +++
html = ("<p style='color:blue;'> Completed: <b>%s </b> </p> <br>") % i
QtCore.QThread.msleep(5)
ind = ind if i%100!=0 else ind+1 # +++
signal.emit(html, ind) # +++

def mySignalHandler(html, val): # Called to process a signal
plainTextEdit.appendHtml(html)
progressBar.setValue(val)

class WindowSignal(QtWidgets.QWidget):
""" New signals can be defined as class attributes using the pyqtSignal() factory. """
my_signal = QtCore.pyqtSignal(str, int, name='my_signal')

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)

window = WindowSignal()

button = QtWidgets.QPushButton("Emit your signal!", window)
button.clicked.connect(lambda: processing(window.my_signal))

# since you do not publish test.ui, I replaced it with the line below:
plainTextEdit = QtWidgets.QPlainTextEdit(window)
progressBar = QtWidgets.QProgressBar()
progressBar.setTextVisible(False)

layoutHBox = QtWidgets.QHBoxLayout()
layoutHBox.addWidget(button)
layoutHBox.addWidget(plainTextEdit)

layoutVBox = QtWidgets.QVBoxLayout()
window.setLayout(layoutVBox)
layoutVBox.addLayout(layoutHBox)
layoutVBox.addWidget(progressBar)

window.my_signal.connect(mySignalHandler, QtCore.Qt.QueuedConnection)

window.show()

sys.exit(app.exec_())

Sample Image

Example 2:

test.ui is a file containing the description of the main window automatically generated by Qt Designer.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<widget class="QWidget" name="">
<property name="geometry">
<rect>
<x>12</x>
<y>12</y>
<width>371</width>
<height>281</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="button">
<property name="text">
<string>Emit your signal!</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="plainTextEdit"/>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
<property name="invertedAppearance">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

main.py

import sys
import threading
from PyQt5 import QtWidgets, QtCore, uic

def thread(my_func):
""" Runs a function in a separate thread. """
def wrapper(*args, **kwargs):
my_thread = threading.Thread(target=my_func, args=args, kwargs=kwargs)
my_thread.start()
return wrapper

@thread
def processing(signal):
""" Emulates the processing of some data. """
for i in range (1,101): # (1,100000)
html = ("<p style='color:blue;'> Completed: <b>%s </b> </p> <br>") % i
QtCore.QThread.msleep(10)
signal.emit(html, i) # Send a signal in which we transfer the received data

def mySignalHandler(html, val): # Called to process a signal
window.plainTextEdit.appendHtml(html)
window.progressBar.setValue(val)

class WindowSignal(QtWidgets.QWidget):
""" New signals can be defined as class attributes using the pyqtSignal() factory. """
my_signal = QtCore.pyqtSignal(str, int, name='my_signal')

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)

window = WindowSignal()
uic.loadUi('test.ui', window)

window.button.clicked.connect(lambda: processing(window.my_signal))
window.my_signal.connect(mySignalHandler, QtCore.Qt.QueuedConnection)

window.show()

sys.exit(app.exec_())

Sample Image

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_())

Updating a QLCDNumber in PyQt via Background Thread

You are free to use Python or PyQt threading modules, bot of them work fine, but if you are new to threading I wold recommend to use Qt thread module because it is easier to setup, and If you use Qt Lib. it is easier to implement it in.
Even if this is not a complete answer I hope it helps, PS: try to put some code in the question it may help others to test things out

Pause and Resume QThread

Generally speaking, you don't usually need to "pause" the thread, but to properly queue the processing based on the needed preload count. The thread will then "pause" itself, but only because it's waiting for new objects to process.

Assuming that the processing is always sequential and in one direction (no going back to the previous image), a possible solution is to always queue the n + preload-count item every time a new image is shown.

from queue import Queue
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class Loader(QThread):
imageReady = pyqtSignal(int, QImage)
def __init__(self):
super().__init__()
self.queue = Queue()

def run(self):
self.keepRunning = True
while self.keepRunning:
index, path = self.queue.get()
image = QImage(path)
# simulate long processing
self.msleep(image.width() + image.height())
result = image.scaled(600, 600, Qt.KeepAspectRatio)
self.imageReady.emit(index, result)

def process(self, index, path):
self.queue.put((index, path))

def stop(self):
self.keepRunning = False
self.wait()

class LinearImageProcess(QWidget):
cacheSize = 10
currentIndex = -1
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.startButton = QPushButton('Start')
self.continueButton = QPushButton('Continue', enabled=False)
self.view = QLabel()
self.view.setAlignment(Qt.AlignCenter)
self.view.setFixedSize(600, 600)
layout.addWidget(self.startButton)
layout.addWidget(self.continueButton)
layout.addWidget(self.view)

self.fileList = tuple(enumerate(
QDir(QDir.tempPath()).entryList(['*.jpg', '*.png'])))
self.cache = []
self.loader = Loader()
self.loader.start()
self.loader.imageReady.connect(self.imageReady)

self.startButton.clicked.connect(self.start)
self.continueButton.clicked.connect(self.showNext)

def start(self):
self.startButton.setEnabled(False)
self.currentIndex = 0
for i, file in self.fileList[:self.cacheSize]:
self.loader.process(i, file)

def showNext(self):
if self.cache:
self.view.setPixmap(self.cache.pop(0))
else:
self.continueButton.setEnabled(False)
nextIndex = self.currentIndex + 1
if nextIndex < len(self.fileList):
self.currentIndex = nextIndex
nextProcess = self.currentIndex + self.cacheSize
if nextProcess < len(self.fileList):
self.loader.process(*self.fileList[nextProcess])

def imageReady(self, index, image):
pm = QPixmap.fromImage(image)
if self.currentIndex == index:
self.view.setPixmap(pm)
self.continueButton.setEnabled(self.currentIndex < len(self.fileList))
else:
self.cache.append(pm)
self.continueButton.setEnabled(True)
if self.continueButton.isEnabled():
self.continueButton.setFocus()

app = QApplication([])
w = LinearImageProcess()
w.show()
app.exec()

If you, instead, want to provide browsing/processing in both directions, things get a bit tricky: the problem is that the thread queue might need to change while it's processing because the user has changed browsing direction (or even skipped to another index).

A possible solution is to implement a further queue for the UI that depends on the current index. This results in the UI thread requesting a new process only when a new image has been received from the thread, and then queue the next process request only based on the current index. A benefit of this approach is that it would obviously work even for the simpler "linear" direction used above.

class NonLinearImageProcess(QWidget):
cacheSize = 20
_currentIndex = -1
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.startButton = QPushButton('Start')

self.browseWidget = QWidget(enabled=False)
browseLayout = QHBoxLayout(self.browseWidget)
browseLayout.setContentsMargins(0, 0, 0, 0)
style = self.style()
self.beginButton = QToolButton(
icon=style.standardIcon(style.SP_MediaSkipBackward))
self.backButton = QToolButton(
icon=style.standardIcon(style.SP_ArrowLeft))
self.imageSpin = QSpinBox(keyboardTracking=False)
self.forwardButton = QToolButton(
icon=style.standardIcon(style.SP_ArrowRight))
self.endButton = QToolButton(
icon=style.standardIcon(style.SP_MediaSkipForward))

browseLayout.addWidget(self.beginButton)
browseLayout.addWidget(self.backButton)
browseLayout.addWidget(self.imageSpin)
browseLayout.addWidget(self.forwardButton)
browseLayout.addWidget(self.endButton)

self.view = QLabel()
self.view.setAlignment(Qt.AlignCenter)
self.view.setFixedSize(600, 600)

layout.addWidget(self.startButton)
layout.addWidget(self.browseWidget)
layout.addWidget(self.view, alignment=Qt.AlignCenter)

self.fileList = tuple(enumerate(
QDir(QDir.tempPath()).entryList(['*.jpg', '*.png'])))
self.imageSpin.setRange(1, len(self.fileList))
self.imageSpin.setSuffix(' of {}'.format(len(self.fileList)))

self.cache = {}
self.loadQueue = []
self.loader = Loader()
self.loader.start()
self.loader.imageReady.connect(self.imageReady)
self.goToTimer = QTimer(self, singleShot=True,
interval=100, timeout=self.goTo)

self.startButton.clicked.connect(self.start)
self.beginButton.clicked.connect(self.goToStart)
self.backButton.clicked.connect(self.goToPrevious)
self.forwardButton.clicked.connect(self.goToNext)
self.endButton.clicked.connect(self.goToEnd)
# the lambda is required since valueChanged sends an integer argument
# that would be used as interval for start()
self.imageSpin.valueChanged.connect(lambda: self.goToTimer.start())

self.setMaximumSize(self.sizeHint())

@pyqtProperty(int)
def currentIndex(self):
return self._currentIndex

@currentIndex.setter
def currentIndex(self, index):
if index < 0:
index = 0
maxIndex = len(self.fileList) - 1
if index > maxIndex:
index = maxIndex
if self._currentIndex == index:
return

self._currentIndex = index
canGoBack = index > 0
self.beginButton.setEnabled(canGoBack)
self.backButton.setEnabled(canGoBack)
canGoForward = index < maxIndex
self.forwardButton.setEnabled(canGoForward)
self.endButton.setEnabled(canGoForward)

def start(self):
self.startButton.setEnabled(False)
if not self.fileList:
return
self.currentIndex = 0
self.loader.process(*self.fileList[0])
for i, file in self.fileList[1:self.cacheSize // 2]:
self.loadQueue.append(i)

def goTo(self, index=None):
if index is None:
index = self.imageSpin.value() - 1
oldIndex = self.currentIndex
self.currentIndex = index
if oldIndex == self.currentIndex:
return

pm = self.cache.get(index)
if isinstance(pm, QPixmap):
self.view.setPixmap(self.cache[index])
self.browseWidget.setEnabled(True)
self.imageSpin.setFocus()
with QSignalBlocker(self.imageSpin):
self.imageSpin.setValue(index + 1)
else:
self.browseWidget.setEnabled(False)

partCacheSize = self.cacheSize // 2
start = max(0, index - partCacheSize)
end = min(index + partCacheSize + 1, len(self.fileList))
for i in range(start):
if isinstance(self.cache.get(i), QPixmap):
self.cache.pop(i)
for i in range(end, len(self.fileList)):
if isinstance(self.cache.get(i), QPixmap):
self.cache.pop(i)

self.loadQueue.clear()
# try to equally process images from both "edges" of the queue: i.e. if
# the current index is 30, queue indexes 31, 29, 32, 28, etc.
for pair in zip_longest(range(index - 1, start - 1, -1), range(index, end)):
for i in pair:
if (i is not None and i not in self.cache):
self.loadQueue.append(i)
self.processNextInQueue()

def processNextInQueue(self):
if self.loadQueue:
nextIndex = self.loadQueue.pop(0)
self.loader.process(*self.fileList[nextIndex])

def goToStart(self):
self.goTo(0)

def goToPrevious(self):
self.goTo(self.currentIndex - 1)

def goToNext(self):
self.goTo(self.currentIndex + 1)

def goToEnd(self):
self.goTo(len(self.fileList) - 1)

def imageReady(self, index, image):
pm = QPixmap.fromImage(image)
self.cache[index] = pm
if self.currentIndex == index:
self.view.setPixmap(pm)
self.browseWidget.setEnabled(True)
self.imageSpin.setFocus()
with QSignalBlocker(self.imageSpin):
self.imageSpin.setValue(index + 1)
if index in self.loadQueue:
self.loadQueue.remove(index)
self.processNextInQueue()


Related Topics



Leave a reply



Submit