pythonqapplication

Terminate application if subprocess ends


I have an application that is doing some data processing in its main thread. So far it was a pure console application. Now I had to add a QT App for visualization purpose and did this as a separate thread.

If the QT Window is closed, the main thread of course still runs. How could I terminate the main thread once the window is closed?

class Window(threading.Thread)
  def __init__(self, data_getter):
      super(Window, self).__init__()
      self.getter = data_getter


  def update(self):
      data = self.getter()
      #update all UI widgets

  def run(self):

      app: QApplication = QApplication([])

      app.setStyleSheet(style.load_stylesheet())
      window = QWidget()
      window.setWindowTitle("Test Widget")
      window.setGeometry(100, 100, 600, 300)

      layout = QGridLayout()
      self.LABEL_state: QLabel = QLabel("SM State: N/A")
      layout.addWidget(self.LABEL_state)
      window.setLayout(layout)
      window.show()

      timer = QTimer()
      timer.timeout.connect(self.update)
      timer.start(1000)

      app.exec_()
        
class Runner:
  def __init__(self)
    pass
      
  def data_container(self):
    return data
    
  def process_data(self):
    #do the data processing

def main():
    runner: Runner = Runner()

    time.sleep(1)

    w = Window(runner.data_container)
    w.start()

    while True:
        runner.process_data()
        time.sleep(2)

      
if __name__ == "__main__": main()

The best idea I had is to give Window another function reference of Runner that is then registered inside Window to atexit and would set a termination flag that is frequently checked inside the main process (Runner). Is there a better approach? I know it migth be better to have the QApp run as the main process, but I'd like to not have to do that in this case.


Solution

  • There are basically two questions here: synchronising an event accross two threads, and stopping a running thread from outside. The way you solve the latter problem will probably affect your solution to the former. Very broadly, you can either:

    In either case you can then design your api however you like, but a .stop() or .cancel() method is a very normal solution.

    The trouble with relying on polling is that the worse case response time is an entire cycle of your main loop. If that's not acceptable you probably want to trigger the containing process or look for ways to check more frequently (if your process_data() takes << 2s to run, replace the sleep(2) with a looped smaller delay and poll the flag there).

    If stopping by setting a flag isn't workable, you can trigger the containing process. This normally implies that the triggering code is running in a different thread/process. Python's threads don't have a .terminate(), but multiprocessing.Processes do, so you could delegate your processing over to a process and then have the main code call .terminate() (or get the pid yourself and send the signal manually). In this case the main code would be doing nothing until signalled, or perhaps nothing at all.

    Lastly, communication between the graphical thread and the processing thread depends on how you implement the rest. For simply setting a flag, exposing a method is fine. If you move the processing code to a Process and have the main thread idle, use a blocking event to avoid busy-looping.

    And yes, it would be easier if the graphical thread were the main thread and started and stopped the processing code itself. Unless you know this will greatly complicate things, have a look at it to see how much you would need to change to do this: well designed data processing code should just take data, process it, and push it out. If putting it in a thread is hard work, the design probably needs revisiting. Lastly there's the 'nuclear option' of just getting the pid of the main thread inside your window loop and killing it. That's horribly hacky, but might be good enough for a demonstration job.