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_()
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()