multithreading - stop - semaforos python
PyQt: ¿Cómo enviar una señal de detención a un hilo donde un objeto ejecuta un ciclo while condicionado? (2)
Estoy haciendo un multi-threading. Tengo una clase de trabajadores con un método de work
, que envío a un QThread
separado. El método de work
tiene un ciclo while acondicionado dentro. Quiero poder enviar una señal al objeto trabajador para detenerlo (cambiando la condición _running
a falso). Esto hará que el ciclo while salga y que se envíe una señal terminada desde el objeto trabajador (que está conectado a la ranura de salida del hilo del trabajador).
La condición falsa se envía al objeto trabajador a través de una señal, pero nunca se recibe, lo que creo es porque el ciclo while bloquea el bucle de evento de su hilo. Incluso si puse QCoreApplication.processEvents()
dentro del ciclo while, no ocurre nada. ¿Dónde está el problema? ¿Por qué no se procesa la señal? (Observe que la instrucción de impresión en la ranura de detención del Trabajador nunca se ejecuta, pero lo extraño es que el hilo parece detenerse de forma incorrecta).
Aquí está el código:
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)
Las conexiones de señal / ranura de hilo cruzado requieren un bucle de evento en ejecución en el hilo del objeto receptor.
En su caso, hay un bucle de evento en el segundo hilo y se está ejecutando, pero en todo momento está ejecutando su método de work
y nunca regresa desde allí.
Por lo tanto, todos los eventos de invocación de ranura están bloqueados en la cola de eventos del bucle de eventos.
Si quieres hackear esto, como lo hiciste con QCoreApplication.processEvents
, podrías intentar obtener el eventDispatcher
del eventDispatcher
y llamar a su processEvent
.
Si solo necesita finalizar al trabajador, puede llamar al requestInteruption
del hilo y en lugar de verificar self._running
, compruebe que self._running
del hilo.
Hay dos problemas principales en su ejemplo:
En primer lugar, está emitiendo una señal para detener al trabajador, pero como la señal es de hilo cruzado, se publicará en la cola de eventos del receptor. Sin embargo, el trabajador está ejecutando un bloqueo while-loop, por lo que los eventos pendientes no se pueden procesar. Hay algunas formas de evitar esto, pero probablemente el más simple es simplemente llamar al método de stop
del trabajador directamente en lugar de usar una señal.
En segundo lugar, no está ejecutando explícitamente un bucle de evento en el hilo principal, por lo que las señales de hilo cruzado enviadas por el trabajador no se pueden poner en cola. Más importante aún, sin embargo, tampoco hay nada que impida que el programa salga después de que el usuario presiona una tecla, por lo que el cliente y el trabajador serán recogidos de inmediato.
A continuación hay una versión reescrita de su ejemplo que soluciona todos los problemas:
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()
@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_())