0

I am writing a GUI using pyqt5 (Python 3.6). I am trying to run another thread in parallel of the main GUI. I would like this child thread to terminate when I close the main application. In this example, the child thread is a simple counter. When I close the main GUI, the counter still keeps going. How can I get the thread to end when the GUI window is closed? In the real case I may have a thread that is running operations that takes a few minutes to execute. I am reluctant to use a flag within the thread to assess if it should end because it may take minutes for the thread to close after the GUI window has been closed. I would prefer the thread to end right away. Any suggestions? Thank you.

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
import time, threading, sys

class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize th widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)

    def on_click(self):
        self.worker = threading.Thread(target=Worker)
        self.worker.daemon = True
        self.worker.start()

def Worker(count=1):
    while count>0:
        print(count)
        time.sleep(2)
        count+=1

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()
pyD
  • 1
  • 1
  • Well, for starters, it's always a good thing to use Qt's own threading using [QThread](https://doc.qt.io/qt-5/qthread.html) even within Python. Then, there's also [`terminate()`](https://doc.qt.io/qt-5/qthread.html#terminate), but it's usually not a good idea to kill a thread while it's running unless *you really know what you're doing*, in **any** case. – musicamante Apr 11 '20 at 02:38
  • I tried to add terminate (self.worker.terminate()) to the closeEvent of the application but it throws an error: AttributeError: 'Thread' object has no attribute 'terminate'. def closeEvent(self,event): print('Closing') self.worker.terminate() event.accept() – pyD Apr 11 '20 at 02:41
  • From your reply I'd say you're still using Python's `threading.Thread` object, and **not** the QThread object as suggested. – musicamante Apr 11 '20 at 02:46
  • Indeed. Missed that I will try it. – pyD Apr 11 '20 at 02:49
  • in current example you could use `while count>0 and running:` and global variable `running = True`. When you set `running = False` then it should stop loop and then thread should end automatically. But not in all situations it is solution. – furas Apr 11 '20 at 02:49
  • Yes. Sorry. I iwill try that. – pyD Apr 11 '20 at 02:54
  • I have tried the while loop solution with the global variable and it definitely works. My main concern is that the thread may take a while to end if the steps within the while loop are long, I tried the QThread solution but this locks up the main GUI. I may not be using it properly. I am posting my code next. – pyD Apr 11 '20 at 03:03

3 Answers3

0

I tried to use the QThread but this locks up the main GUI. I am not sure if I am implementing it correctly.

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
from PyQt5.QtCore import QThread
import time, threading, sys

class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize th widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)

    def on_click(self):
        self.worker = Worker()
        self.worker.run()

    def closeEvent(self,event):
        print('Closing')
        self.worker.terminate()
        event.accept()

class Worker(QThread):

    def __init__(self):
        QThread.__init__(self)

    def run(self):
        count=1
        while count>0:
            print(count)
            time.sleep(2)
            count+=1

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()
pyD
  • 1
  • 1
0
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
from PyQt5.QtCore import QThread,QObject
import time, threading, sys

class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize th widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)

    def on_click(self):
        self.my_thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.my_thread)
        self.my_thread.started.connect(self.worker.run)
        self.my_thread.start()        

    def closeEvent(self,event):
        print('Closing')

class Worker(QObject):

    def __init__(self):
        super().__init__()

    def run(self):
        count=1
        while count>0:
            print(count)
            time.sleep(2)
            count+=1

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()
0

hope self.my_thread.requestInterruption() won't qualify as flag, otherwise is not the sought answer :

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
from PyQt5.QtCore import QThread,QObject
import time, threading, sys

class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize th widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)

    def on_click(self):
        self.my_thread = QThread()
        self.worker = Worker(self.my_thread)
        self.worker.moveToThread(self.my_thread)
        self.my_thread.started.connect(self.worker.run)
        self.my_thread.start()        

    def closeEvent(self,event):
        
        self.my_thread.requestInterruption()
        print('Closing')
        self.my_thread.quit()
        
        self.my_thread.wait()
        
        print('is thread running ?', self.my_thread.isRunning())

class Worker(QObject):

    def __init__(self, thread):
        super().__init__()
        
        self.thread = thread

    def run(self):
        count=1
        while count>0:
            print(count)
            time.sleep(2)
            count+=1
            
            if self.thread.isInterruptionRequested():
                break

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()

see PySide6.QtCore.QThread.isInterruptionRequested()


Using threading.thread like in your question, I used threading.Event() and I set it (= True) to stop your Worker loop, but once again I hope is not a flag:

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
 

import time, threading, sys

class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize th widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)
        
        self.event = threading.Event()

    def on_click(self):
        
        self.worker = threading.Thread(target=Worker, args = (self.event,))
        self.worker.daemon = True
        self.worker.start()
        
    def closeEvent(self, event):
        
        if 'worker' in self.__dict__:
        
            self.event.set()     ## sets event: threading.Event() = True
            self.worker.join()   ## waits self.worker to complete *** 
        
            print('is worker alive, self.worker.is_alive() : ', self.worker.is_alive())
        
        pass
        
        

def Worker(event, count=1 ):
    
    print(event)
    while count>0:
        print(count)
        time.sleep(2)
        count+=1
        
        if event.is_set():
            break

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()

try the code commenting and uncommentig out self.event.set() & self.worker.join() in

def closeEvent(self, event):
        
        if 'worker' in self.__dict__:
        
            self.event.set()     ## sets event: threading.Event() = True
            self.worker.join()   ## waits self.worker to complete *** 
        
            print('is worker alive, self.worker.is_alive() : ', self.worker.is_alive())
        
        pass

*** Wait until the thread terminates. This blocks the calling thread until the thread whose join() method is called terminates –

pippo1980
  • 2,181
  • 3
  • 14
  • 30