Delay in signal from thread

Heads up! You've already completed this tutorial.

Minsky | 2021-04-08 19:11:24 UTC | #1

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:

[code] class Worker(QRunnable): def init(self, fn=None, args, *kwargs): super().init()

python
            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

[/code]

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?


PedanticHacker | 2021-04-08 23:55:19 UTC | #2

Hello, Minsky, welcome to the forum! :)

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:

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

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.


Minsky | 2021-04-08 23:59:17 UTC | #3

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.

[code] class MainWindow(QMainWindow): def init(self): super().init()

python
        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

[/code]

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.


PedanticHacker | 2021-04-09 07:46:04 UTC | #4

Can you try updating PyQt to version 5.15.3 and trying self.threadpool.start(do_this)?

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.


Minsky | 2021-04-09 11:27:15 UTC | #5

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?


PedanticHacker | 2021-04-09 12:21:05 UTC | #6

python
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().__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.

python
class Worker(QRunnable):
    ...

    @pyqtSlot()  # A cruical part!
    def run(self):
        ...

Minsky | 2021-04-09 14:34:36 UTC | #7

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 and runs in a separate thread as expected. However, if it is passed with arguments, it for some reason, becomes a and somehow runs in the main thread instead, therefore locking the main event loop until it ends.

[code] @pyqtSlot() def run(self): print(type(self.fn)) if self.fn is not None: self.fn(self.args, *self.kwargs) [/code]

I haven't a clue why this is happening. Any ideas?


PedanticHacker | 2021-04-09 16:25:56 UTC | #8

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:

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

Maybe this helps?


PedanticHacker | 2021-04-09 16:45:34 UTC | #9

If you still don't succeed, study this code, provided by our good friend Martin Fitzpatrick (@martin):

python
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().__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().__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_()

Minsky | 2021-04-09 17:11:56 UTC | #10

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:

[code] def call_do_this(self): worker = Worker(self.model.do_this('This is a message')) # Passing function call self.threadpool.start(worker) [/code]

Do this:

[code] 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 [/code]

@ 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.


PedanticHacker | 2021-04-09 17:14:48 UTC | #11

Wow, congratz, the beast has been slayed! 👍 Good job! We all learn as we go, don't we? 😉

I'm proud of you, keep up the good work. 🙂


Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Delay in signal from thread was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.