2

I intend to have a GUI where one (later three) threads read live data from different sources with an adjustable interval (e.g. 10s) and plot these data in the main window.

I am using PyQt5 and python 3.6.

The reading is performed in an infinite loop in a worker thread as such:

class ReadingThread(QtCore.QObject):
    output = QtCore.pyqtSignal(object)

    def __init__(self, directory, interval):
        QtCore.QObject.__init__(self)
        self.directory=directory
        self.stillrunning = True
        self.refreshtime = interval
    

    def run(self):
        print('Entered run in worker thread')
        self.stillrunning = True

        while self.stillrunning:
            outstring=self.read_last_from_logfile()  # data reader function, not displayed

            self.output.emit(outstring)
            time.sleep(self.refreshtime)


    @QtCore.pyqtSlot(int)  # never called as loop is blocking?
    def check_break(self, val):
        if val:
            self.stillrunning=False
        else:
            self.stillrunning = True

The main thread looks like this, start() and stop() are called via pushButtons:

class Window(QtWidgets.QMainWindow, MainWindow.Ui_MainWindow):
    def __init__(self, directory, interval):
        super(Window, self).__init__()
        self.thread = QtCore.QThread()
        self.worker = ReadingThread(directory, interval)

    emit_stop=QtCore.pyqtSignal(int)

    def start(self):
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.output.connect(self.print_new_value) 
        self.emit_stop.connect(self.worker.check_break)

        self.thread.start()

    def stop(self):
        self.emit_stop.emit(1)
        # time.sleep(11)
        if self.thread.isRunning(): #did not work either
            self.thread.quit()

        if self.thread.isRunning(): #did also not work
            self.thread.terminate()
        return

    def print_new_value(self, value): #test function for output of values read by worker thread, working well
        print (value)
        return

def main():
    app = QtWidgets.QApplication(sys.argv)                                                           
    interval=10  #read every 10s new data
    directory="/home/mdt-user/logfiles/T_P_logs"
    gui = Window(directory,interval)
    gui.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

My problem is: How can I let the worker thread in the loop look for incoming signals issued by the main thread? Or differently phrased: How can I have a status variable like my self.stillrunning which can be set/accessed outside the worker thread but checked within the worker thread?

I want to avoid killing the thread with means like self.thread.terminate(), which I did unsuccessfully try.

Help would very much be appreciated. I did a search of course, but the answers given and/or problems stated were either too lengthy for what I assume has to be a simple solution, or not applicable.

g_h_e
  • 21
  • 1
  • 3
  • You can already access `stillrunning`: `self.worker.stillrunning = False` – musicamante Jun 28 '21 at 15:32
  • Thank you very much, that's exactly what I needed! I was missing that completely. I now added a `finished` signal when the `while` loop breaks to quit and delete the worker. Can I upvote your answer somehow? – g_h_e Jun 28 '21 at 16:18

1 Answers1

5

I don’t see how above comment could have solved your issue. By design your worker will not be able to receive signals, as the while loop blocks the worker event loop until it breaks and the method finishes. Then all the (throughout the blockage) received signals will be worked. Well, technically your received signals aren’t blocked, they’re just not getting worked, until the event loop is being worked again...
I see two solutions, that work with your design pattern (utilizing move to thread).

Solution 1: QTimer (clean, more QT like solution)

The idea here is to use a QTimer. You provide a time period (in milliseconds) to this timer and every time this period is passed, said timer will perform a task (i.e. call a method/function). As you can even pass 0 ms as time period, you can emulate a while loop like behavior. The upside: The event loop won’t be blocked and after every timeout, received signals will be worked.
I modified your code realizing this solution via QTimer. I think with the code comments this example is somewhat self-explanatory.

class ReadingThread(QtCore.QObject):

    output = QtCore.pyqtSignal(object)
    
    def __init__(self, directory, interval):
        #QtCore.QObject.__init__(self)
        super(ReadingThread, self).__init__() # this way is more common to me
        
        self.directory = directory
        self.refreshtime = interval

        # setting up a timer to substitute the need of a while loop for a 
        # repetitive task
        self.poller = QTimer(self)
        # this is the function the timer calls upon on every timeout
        self.poller.timeout.connect(self._polling_routine)

    def _polling_routine(self):
        # this is what's inside of your while loop i.e. your repetitive task
        outstring = self.read_last_from_logfile()
        self.output.emit(outstring)

    def polling_start(self):
        # slot to call upon when timer should start the routine.
        self.poller.start(self.refreshtime)
        # the argument specifies the milliseconds the timer waits in between
        # calls of the polling routine. If you want to emulate the polling
        # routine in a while loop, you could pass 0 ms...

    def polling_stop(self):
        # This simply stops the timer. The timer is still "alive" after.
        self.poller.stop()

    # OR substitute polling_start and polling_stop by toggling like this:
    def polling_toggle(self):
        poller_active = self.poller.isActive()
        if poller_active:
            # stop polling
            self.poller.stop()
        else:
            # start polling
            self.poller.start(self.refreshtime)


class Window(QtWidgets.QMainWindow, MainWindow.Ui_MainWindow):
    
    emit_start =  QtCore.pyqtSignal()
    emit_stop = QtCore.pyqtSignal()
    
    def __init__(self, directory, interval):
        super(Window, self).__init__()
        self.init_worker()

    def init_worker(self):
        self.thread = QtCore.QThread()
        
        self.worker = ReadingThread(directory, interval)
        self.worker.moveToThread(self.thread)

        self.worker.output.connect(self.print_new_value) 
        self.emit_start.connect(self.worker.polling_start)
        self.emit_stop.connect(self.worker.polling_stop)

        self.thread.start()

    def start_polling(self):
        self.emit_start.emit()

    def stop_polling(self):
        self.emit_stop.emit()

    def finish_worker(self):
        # for sake of completeness: call upon this method if you want the
        # thread gone. E.g. before closing your application.
        # You could emit a finished sig from your worker, that will run this.
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)

    def print_new_value(self, value):
        print(value)

For a better look at how to do this cleanly with QThread (the complexity here is doing the threading right, the QTimer is relatively trivial): https://realpython.com/python-pyqt-qthread/#using-qthread-to-prevent-freezing-guis
EDIT: Make sure to check out the documentation of the QTimer. You can dynamically set the timeout period and so much more.

Solution 2: Passing a mutable as a control variable

You can e.g. pass a dictionary with a control variable into your worker class/thread and use it to break the loop. This works, as (oversimplified statements follow) threads share common memory and mutable objects in python share the same object in memory (this has been more than thoroughly discussed on SO). I’ll illustrate this here in your modified code, also illustrated you'll find that the memory id is the same for your control dicts in the main and the worker thread:

class ReadingThread(QtCore.QObject):
    output = QtCore.pyqtSignal(object)

    def __init__(self, directory, interval, ctrl):
        QtCore.QObject.__init__(self)
        self.ctrl = ctrl # dict with your control var
        self.directory = directory
        self.refreshtime = interval 

    def run(self):
        print('Entered run in worker thread')
        print('id of ctrl in worker:', id(self.ctrl))
        self.ctrl['break'] = False

        while True:
            outstring=self.read_last_from_logfile()
            self.output.emit(outstring)
            
            # checking our control variable
            if self.ctrl['break']:
                print('break because flag raised')
                # might emit finished signal here for proper cleanup
                break # or in this case: return

            time.sleep(self.refreshtime)


class Window(QtWidgets.QMainWindow, MainWindow.Ui_MainWindow):
    
    emit_stop=QtCore.pyqtSignal(int)
    
    def __init__(self, directory, interval):
        super(Window, self).__init__()
        self.thread = QtCore.QThread()
        self.ctrl = {'break': False} # dict with your control variable
        print('id of ctrl in main:', id(self.ctrl))

        # pass the dict with the control variable
        self.worker = ReadingThread(directory, interval, self.ctrl)

    def start(self):
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.output.connect(self.print_new_value) 

        self.thread.start()

    def stop(self):
        # we simply set the control variable (often refered to as raising a flag)
        self.ctrl['break'] = True

This solution almost requires no changes to your code, and I would definitely consider it not QT like or even clean, but it is extremely convenient. And sometimes you don’t want to code your experiment/long running task around the fact that you’re using a GUI toolkit.
This is the only way I know of, that let’s you get around the blocked event loop. If somebody has a cleaner solution to this, please let the world know. Especially as this is the only way, to break out of your long running task in a controlled manner, from multiple points, as you can check for your control variable multiple times throughout your repetitive routine.

XIII_
  • 393
  • 3
  • 7