Delay in signal from thread

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?

Hello, Minsky, welcome to the forum! :slight_smile:

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.

So, in __init__ of MainWindow:

self.model = Model()
self.model.signals.status.connect(self.update_status_bar)
                
self.button = QPushButton()
self.button.pressed.connect(self.call_do_this)

self.threadpool = QThreadPool()

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.

I hope this helps in any way.

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?

Try removing the default None value of the fn parameter in the __init__ method signature in the Worker class.

So, instead of def __init__(self, fn=None, *args, **kwargs):, have that as def __init__(self, fn, *args, **kwargs):.

Then in the body of the run method in the Worker class, remove the if statement. So, let it be like this:

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

Maybe this helps?

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 :grinning: may help prevent others from making a similar mistake.

Wow, congratz, the beast has been slayed! :+1: Good job! We all learn as we go, don’t we? :wink:

I’m proud of you, keep up the good work. :slightly_smiling_face: