Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terminating QThread gracefully on QDialog reject()

I have a QDialog which creates a QThread to do some work while keeping the UI responsive, based on the structure given here: How To Really, Truly Use QThreads; The Full Explanation. However, if reject() is called (due to the user pressing cancel or closing the dialog) while the thread is still running I get an error:

QThread: Destroyed while thread is still running

What I'd like to happen is for the loop in the worker to break early, then do some cleanup in the background (e.g. clear some queues, emit a signal). I've managed to do this with my own "cancel" function, but how do I get it to play nicely with reject() (and all the many ways it could be called)? I don't want the dialog to block waiting for the cleanup - it should just keep running in the background, then exit gracefully.

See sample code below which exhibits the problem. Any help would be greatly appreciated.

#!/usr/bin/env python

from PyQt4 import QtCore, QtGui
import sys
import time

class Worker(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)

    def process(self):
        # dummy worker process
        for n in range(0, 10):
            print 'process {}'.format(n)
            time.sleep(0.5)
        self.finished.emit()

    finished = QtCore.pyqtSignal()

class Dialog(QtGui.QDialog):
    def __init__(self):
        QtGui.QDialog.__init__(self)
        self.init_ui()

    def init_ui(self):
        self.layout = QtGui.QVBoxLayout(self)
        self.btn_run = QtGui.QPushButton('Run', self)
        self.layout.addWidget(self.btn_run)
        self.btn_cancel = QtGui.QPushButton('Cancel', self)
        self.layout.addWidget(self.btn_cancel)

        QtCore.QObject.connect(self.btn_run, QtCore.SIGNAL('clicked()'), self.run)
        QtCore.QObject.connect(self.btn_cancel, QtCore.SIGNAL('clicked()'), self.reject)

        self.show()
        self.raise_()

    def run(self):
        # start the worker thread
        self.thread = QtCore.QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
        self.thread.start()

def main():
    app = QtGui.QApplication(sys.argv)
    dlg = Dialog()
    ret = dlg.exec_()

if __name__ == '__main__':
    main()
like image 685
Snorfalorpagus Avatar asked Apr 14 '13 22:04

Snorfalorpagus


1 Answers

Your problem is: self.thread is freed by Python after the dialog is closed or the cancel button is pressed, while Qt thread is still running.

To avoid such situation, you can designate a parent to that thread. For example,


    def run(self):
        # start the worker thread
        self.thread = QtCore.QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
        self.thread.start()

Then it will be owned by Qt instead of PyQt and hence won't be collected by GC before it is terminated by Qt gracefully. Actually, this method just lets Qt not complain and doesn't solve the problem completely.

To terminate a thread gracefully, the common approach is using a flag to inform the worker function to stop.
For example:

class Worker(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)

    def process(self):
        # dummy worker process
        self.flag = False
        for n in range(0, 10):
            if self.flag:
                print 'stop'
                break
            print 'process {}'.format(n)
            time.sleep(0.5)
        self.finished.emit()

    finished = QtCore.pyqtSignal()

class Dialog(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)
        self.init_ui()

    def init_ui(self):
        self.layout = QtGui.QVBoxLayout(self)
        self.btn_run = QtGui.QPushButton('Run', self)
        self.layout.addWidget(self.btn_run)
        self.btn_cancel = QtGui.QPushButton('Cancel', self)
        self.layout.addWidget(self.btn_cancel)

        QtCore.QObject.connect(self.btn_run, QtCore.SIGNAL('clicked()'), self.run)
        QtCore.QObject.connect(self.btn_cancel, QtCore.SIGNAL('clicked()'), self.reject)

        QtCore.QObject.connect(self, QtCore.SIGNAL('rejected()'), self.stop_worker)

        self.show()
        self.raise_()

    def stop_worker(self):
        print 'stop'
        self.worker.flag = True

    def run(self):
        # start the worker thread
        self.thread = QtCore.QThread(self)
        self.worker = Worker()
        self.worker.moveToThread(self.thread)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('started()'), self.worker.process)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.thread.quit)
        QtCore.QObject.connect(self.worker, QtCore.SIGNAL('finished()'), self.worker.deleteLater)
        QtCore.QObject.connect(self.thread, QtCore.SIGNAL('finished()'), self.thread.deleteLater)
        self.thread.start()
like image 163
nymk Avatar answered Sep 20 '22 23:09

nymk