multithreadingpython-2.7pyqt4signals-slotsqthread

PyQt: How to deal with QThread?


In the following code I try to deal with QThread. In this executable example there are three buttons: first for start, second for stop and third for close. Well, when I start the task its runs like a charm. BUT when I want the while-loop to stop I click on the stop-button. And now, there is a problem: the while-loop doesn't stop.

You see, the stop-button emits a signal to call the stop() method on TestTask().

What is wrong?

from sys import argv

from PyQt4.QtCore import QObject, pyqtSignal, QThread, Qt, QMutex

from PyQt4.QtGui import QDialog, QApplication, QPushButton, \
     QLineEdit, QFormLayout, QTextEdit

class TestTask(QObject):

    def __init__(self, parent=None):
        QObject.__init__(self, parent)

        self._mutex = QMutex()

        self._end_loop = True

    def init_object(self):
        while self._end_loop:
            print "Sratus", self._end_loop

    def stop(self):
        self._mutex.lock()
        self._end_loop = False
        self._mutex.unlock()

class Form(QDialog):
    stop_loop = pyqtSignal()

    def __init__(self, parent=None):
        QDialog.__init__(self, parent)

        self.init_ui()

    def init_ui(self):


        self.pushButton_start_loop = QPushButton()
        self.pushButton_start_loop.setText("Start Loop") 

        self.pushButton_stop_loop = QPushButton()
        self.pushButton_stop_loop.setText("Stop Loop")       

        self.pushButton_close = QPushButton()
        self.pushButton_close.setText("Close")

        layout = QFormLayout()

        layout.addWidget(self.pushButton_start_loop)
        layout.addWidget(self.pushButton_stop_loop)
        layout.addWidget(self.pushButton_close)

        self.setLayout(layout)
        self.setWindowTitle("Tes Window")

        self.init_signal_slot_pushButton()

    def start_task(self):

         self.task_thread = QThread(self)
         self.task_thread.work = TestTask()
         self.task_thread.work.moveToThread(self.task_thread)
         self.task_thread.started.connect(self.task_thread.work.init_object)

         self.stop_loop.connect(self.task_thread.work.stop)

         self.task_thread.start()

    def stop_looping(self):
        self.stop_loop.emit()

    def init_signal_slot_pushButton(self):

        self.pushButton_start_loop.clicked.connect(self.start_task)

        self.pushButton_stop_loop.clicked.connect(self.stop_looping)

        self.pushButton_close.clicked.connect(self.close)



app = QApplication(argv)
form = Form()
form.show()
app.exec_()

Solution

  • The stop_loop signal is converted to an event and posted to the event-queue of the receiving thread. But your worker object is running a blocking while-loop, and this prevents the receiving thread processing any pending events in its event-queue. So the slot connected to the stop_loop signal will never be called.

    To work around this, you could call processEvents in the while-loop to allow the thread to process its pending events:

    def init_object(self):
        while self._end_loop:
            QThread.sleep(1)
            QApplication.processEvents()
            print("Status", self._end_loop)
    

    As stated in the Qt docs, processEvents is thread-safe and it's use is normally only discouraged for the main GUI thread (since it can sometimes have unwanted side-effects on certain widgets). However, this does not apply to worker threads where GUI operations are not supported, and the only events that appear in its event-queue are usually from incoming signals (or perhaps locally created timers).

    However, as an alternative to processEvents, you could also call the worker's stop() method directly. In your example, one way to do that is to use a direct connection, which avoids posting the signal to the event-queue of the worker thread:

    self.stop_loop.connect(self.task_thread.work.stop, Qt.DirectConnection)
    

    Another way would be to connect the button to a second function that explicitly calls stop() within the main thread:

    self.pushButton_stop_loop.clicked.connect(lambda: self.task_thread.work.stop())
    

    Strictly speaking, directly invoking a slot that mutates values may not be thread-safe, since multiple threads could call the slot at the same time. Under certain circumstances, this could lead to undefined behaviour (e.g. if the operation is not atomic - see Qt Re-entrancy and Thread-Safety for more details). This doesn't apply to the current example (which uses a simple, one-way boolean flag), but for the sake of completeness, the value could be protected by a mutex like this:

    class TestTask(QObject):   
        def __init__(self, parent=None):
            super().__init__(parent)
            self._mutex = QtCore.QMutex()
            self._end_loop = True
    
        def stop(self):
            self._mutex.lock()
            self._end_loop = False
            self._mutex.unlock()