I'm doing some multi-threading. I have a worker class with a work
method, which I send into a separate QThread
. The work
method has a conditioned while loop inside. I want to be able to send a signal to the worker object to stop it (changing the _running
condition to false). This will cause the while loop to exit, and a finished signal to be sent from the worker object (which is connected to the quit slot of the worker's thread).
The false condition is sent to the worker object via a signal, but it is never received, which I believe is because the while loop blocks the event-loop of its thread. Even if I put QCoreApplication.processEvents()
inside the while loop, nothing happens. Where is the problem? Why isn't the signal processed? (Notice that the print statement in the stop slot on the Worker is never executed - but the weird thing is, the thread does seem to stop in a wrong way).
Here is the code:
import time, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Worker(QObject):
sgnFinished = pyqtSignal()
def __init__(self, parent):
QObject.__init__(self, parent)
self._running = True
@pyqtSlot()
def stop():
print 'stop signal received, switching while loop condition to false'
self._running = False
@pyqtSlot()
def work(self):
while self._running: #this blocks the thread, if changed to an if clause, thread finishes as expected!
QCoreApplication.processEvents() #this doesn't help!
time.sleep(0.1)
print 'doing work...'
#do some cleanup here, then signal the worker is done
self.sgnFinished.emit()
class Client(QObject):
sgnStop = pyqtSignal()
def __init__(self, parent):
QObject.__init__(self, parent)
self._thread = None
self._worker = None
def toggle(self, enable):
if enable:
if not self._thread:
self._thread = QThread()
self._worker = Worker(None)
self._worker.moveToThread(self._thread)
self._worker.sgnFinished.connect(self.on_worker_done)
self.sgnStop.connect(self._worker.stop)
self._thread.started.connect(self._worker.work)
self._thread.start()
else:
print 'sending stop signal to the worker object'
self.sgnStop.emit() #send a queuedconnection type signal to the worker, because its in another thread
@pyqtSlot()
def on_worker_done(self):
print 'workers job was interrupted manually'
self._thread.quit()
#self._thread.wait() not sure this is neccessary
if __name__ == '__main__':
app = QCoreApplication(sys.argv)
client = Client(None)
client.toggle(True)
raw_input('Press something')
client.toggle(False)
There are two main problems in your example:
Firstly, you are emitting a signal to stop the worker, but since the signal is cross-thread, it will be posted in the receiver's event-queue. However, the worker is running a blocking while-loop, so pending events cannot be processed. There are a few ways to work around this, but probably the simplest is to simply call the worker's stop
method directly instead of using a signal.
Secondly, you are not explicitly running an event-loop in the main thread, so cross-thread signals sent from the worker cannot be queued. More importantly, though, there is also nothing to stop the program exiting after the user presses a key - so the client and worker will be immediately garbage-collected.
Below is a re-written version of your example which fixes all the issues:
import time, sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Worker(QObject):
sgnFinished = pyqtSignal()
def __init__(self, parent):
QObject.__init__(self, parent)
self._mutex = QMutex()
self._running = True
@pyqtSlot()
def stop(self):
print 'switching while loop condition to false'
self._mutex.lock()
self._running = False
self._mutex.unlock()
def running(self):
try:
self._mutex.lock()
return self._running
finally:
self._mutex.unlock()
@pyqtSlot()
def work(self):
while self.running():
time.sleep(0.1)
print 'doing work...'
self.sgnFinished.emit()
class Client(QObject):
def __init__(self, parent):
QObject.__init__(self, parent)
self._thread = None
self._worker = None
def toggle(self, enable):
if enable:
if not self._thread:
self._thread = QThread()
self._worker = Worker(None)
self._worker.moveToThread(self._thread)
self._worker.sgnFinished.connect(self.on_worker_done)
self._thread.started.connect(self._worker.work)
self._thread.start()
else:
print 'stopping the worker object'
self._worker.stop()
@pyqtSlot()
def on_worker_done(self):
print 'workers job was interrupted manually'
self._thread.quit()
self._thread.wait()
if raw_input('\nquit application [Yn]? ') != 'n':
qApp.quit()
if __name__ == '__main__':
# prevent some harmless Qt warnings
pyqtRemoveInputHook()
app = QCoreApplication(sys.argv)
client = Client(None)
def start():
client.toggle(True)
raw_input('Press something\n')
client.toggle(False)
QTimer.singleShot(10, start)
sys.exit(app.exec_())
Cross thread signal/slot connections require a running event loop in the thread of the receiver object.
In your case there is an event loop in the second thread and it is running, but it is at all times executing your work
method and never returns from there.
So all slot invocation events are stuck in the event loop's event queue.
If you want to hack around this, like you attempted with QCoreApplication.processEvents
you could try getting the thread's eventDispatcher
and calling its processEvent
.
If you only need to end the worker, you could call the thread's requestInteruption
and instead of checking for self._running
you check for the thread's isInterruptionRequested
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With