I’m having trouble with sending a signal from a thread within an app that I’m writing.
To keep things simple, let’s assume that the app consists of a main window which contains just a push button and a status bar, and has four classes - the MainWindow class, a Signals class, a Model class - which holds the logic functions, and a Worker class which is used to run separate threads.
When the push button is pressed, it connects to the ‘call_do_this’ function which starts a thread that calls the ‘do_this’ function in the separate Model class. The ‘do_this function’ emits a signal containing a message to be displayed in the main window status bar, and then continues with a long running process (around 14 seconds).
The simplistic code is displayed below:
class Worker(QRunnable):
def __init__(self, fn=None, *args, **kwargs):
super().__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
if self.fn is not None:
self.fn(*self.args, **self.kwargs)
class Signals(QObject):
status = pyqtSignal(str)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.threadpool = QThreadPool()
self.model = Model()
self.model.signals.status.connect(self.update_status_bar)
self.button = QPushButton()
self.button.pressed.connect(self.call_do_this)
def call_do_this(self):
self.worker = Worker(self.model.do_this)
self.threadpool.start(self.worker)
def update_status_bar(self, msg):
self.statusBar().showMessage(msg)
class Model(QObject):
def __init__(self):
super().__init__()
self.signals = Signals()
def do_this(self):
self.signals.status.emit('This is a message')
# continue with long running function
The error that occurs, is that the status bar message should be displayed as soon as the ‘do_this’ function starts, but is displayed only after the function ends. What am I doing wrong?
If you’re using PyQt6 (or the latest version of PyQt5, at least version 5.15), the QThreadPool class has the method start that can also call any function, and QThreadPool reserves a thread and uses it to run the function that you pass to that start method. More about it HERE.
You don’t need to have a runnable at all, just pass your function to start, like in your case the do_this function of your Model class: self.threadpool.start(self.model.do_this). Also, always instanciate a QThreadPool class after all the signals have been connected to slots.
Note: The QThreadPool.start method of PySide doesn’t provide calling a regular function, just of PyQt5 from version 5.15 onward and, of course, of PyQt6.
Thanks PedanticHacker, that did the trick. However, if I change the code so that arguments are passed into the ‘do_this’ function, then this freezes the main event loop, making the GUI unresponsive until the function ends. I can’t work out why this happens or how to solve it.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.model = Model()
self.model.signals.status.connect(self.update_status_bar)
button = QPushButton()
button.pressed.connect(self.call_do_this)
self.threadpool = QThreadPool()
def call_do_this(self):
worker = Worker(self.model.do_this('This is a message'))
self.threadpool.start(worker)
def update_status_bar(self, msg):
self.statusBar().showMessage(msg)
class Model(QObject):
def __init__(self):
super().__init__()
self.signals = Signals()
def do_this(self, msg):
self.signals.status.emit(msg)
# continue with long running function
This has got me totally stumped. Any help would be very welcome. I’m using PyQt5 5.13.1, so unfortunately, I end up with a segmentation fault if I don’t use a QRunnable.
Can you try updating PyQt to version 5.15.3 and trying self.threadpool.start(do_this)?
Note: passing a regular function to start (as opposed to passing a runnable) of QThreadPool class was introduced in PyQt in version 5.15. Your version of PyQt doesn’t have that ability yet. That’s why you get a SegmentationFault.
I’ve tried updating PyQt but I’m getting conflict messages regarding dependencies. I prefer to stick with the current version until I upgrade the OS in June. Can you think of a reason why passing the function with arguments locks the main event loop, while passing the same function without arguments works fine?
class Worker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
@pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
self.fn(*self.args, **self.kwargs)
The code above was taken from THIS article written by our fellow Martin Fitzpatrick (@martin).
A possible culprit in your code is that you don’t decorate the run method of your Worker class with @pyqtSlot(). I think I read a post on some other forum that this is needed when subclassing QRunnable. Try it out and tell me if it works.
class Worker(QRunnable):
...
@pyqtSlot() # A cruical part!
def run(self):
...
I’ve discovered the cause of the error, but not the reason for it. If the ‘do_this’ function is passed without arguments to the Worker, it is passed as a <class ‘method’> and runs in a separate thread as expected. However, if it is passed with arguments, it for some reason, becomes a <class ‘NoneType’> and somehow runs in the main thread instead, therefore locking the main event loop until it ends.
@pyqtSlot()
def run(self):
print(type(self.fn))
if self.fn is not None:
self.fn(*self.args, **self.kwargs)
I haven’t a clue why this is happening. Any ideas?
If you still don’t succeed, study this code, provided by our good friend Martin Fitzpatrick (@martin):
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import time
import traceback, sys
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
Supported signals are:
finished
No data
error
tuple (exctype, value, traceback.format_exc() )
result
object data returned from processing, anything
progress
int indicating % progress
'''
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
'''
Worker thread
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
:param callback: The function callback to run on this worker thread. Supplied args and
kwargs will be passed through to the runner.
:type callback: function
:param args: Arguments to pass to the callback function
:param kwargs: Keywords to pass to the callback function
'''
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
# Add the callback to our kwargs
self.kwargs['progress_callback'] = self.signals.progress
@pyqtSlot()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
# Retrieve args/kwargs here; and fire processing using them
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
layout = QVBoxLayout()
self.l = QLabel("Start")
b = QPushButton("DANGER!")
b.pressed.connect(self.oh_no)
layout.addWidget(self.l)
layout.addWidget(b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.show()
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
def progress_fn(self, n):
print("%d%% done" % n)
def execute_this_fn(self, progress_callback):
for n in range(0, 5):
time.sleep(1)
progress_callback.emit(n*100/4)
return "Done."
def print_output(self, s):
print(s)
def thread_complete(self):
print("THREAD COMPLETE!")
def oh_no(self):
# Pass the function to execute
worker = Worker(self.execute_this_fn) # Any other args, kwargs are passed to the run function
worker.signals.result.connect(self.print_output)
worker.signals.finished.connect(self.thread_complete)
worker.signals.progress.connect(self.progress_fn)
# Execute
self.threadpool.start(worker)
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
app = QApplication([])
window = MainWindow()
app.exec_()
Solved it! The Worker class ‘init’ function expects a function name with the arguments separated by commas, whereas I was passing a function call with arguments. The code below fixed the problem,
Instead of this:
def call_do_this(self):
worker = Worker(self.model.do_this('This is a message')) # Passing function call
self.threadpool.start(worker)
Do this:
def call_do_this(self):
worker = Worker(self.model.do_this, 'This is a message') # Passing function name with
self.threadpool.start(worker) # comma separated argument
@ PedanticHacker: Thank you for your help in getting this problem solved, it was greatly appreciated. Hopefully this thread may help prevent others from making a similar mistake.