python - threading - PyQt: moveToThread no funciona cuando se usa partial() para slot
qthread target python (1)
Estoy construyendo una pequeña aplicación GUI que ejecuta un productor (trabajador) y la GUI consume la salida a pedido y la traza (usando pyqtgraph).
Como el productor es una función de bloqueo (tarda un poco en ejecutarse), yo (supuestamente) lo moví a su propio hilo.
Cuando se llama a QThread.currentThreadId () desde el productor, se emite el mismo número que el hilo de la GUI principal. Entonces, el trabajador se ejecuta primero, y luego todas las llamadas a la función de trazado se ejecutan (porque están en cola en la cola de eventos del mismo hilo). ¿Cómo puedo arreglar esto?
Ejemplo ejecutado con parcial:
gui thread id 140665453623104
worker thread id: 140665453623104
Aquí está mi código completo:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal
import pyqtgraph as pg
import numpy as np
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print(''worker thread id: {}''.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print(''Worker finished'')
self.termino.emit()
class MainWindow(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton(''Start worker'')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange(''x'', True)
pi.enableAutoRange(''y'', True)
self.ge1 = pi.plot(pen=''y'')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.moveToThread(self.workerThread)
self.worker.termino.connect(self.setTermino)
# moveToThread doesn''t work here
self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
# self.btn.clicked.connect(self.worker.run)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino:
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == ''__main__'':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
El problema es que Qt intenta elegir el tipo de conexión (cuando se llama a signal.connect(slot)
) en función del hilo en el que se encuentra la slot
. Debido a que ha envuelto la ranura en el QThread con partial
, la ranura a la que se está conectando reside en MainThread (el hilo GUI). Puede anular el tipo de conexión (como el segundo argumento para connect()
pero eso no ayuda porque el método creado por partial
siempre existirá en MainThread, por lo que establecer el tipo de conexión por Qt.QueuedConnection
no ayuda.
La única forma de evitar esto que puedo ver es configurar una señal de relevo, cuyo único propósito es cambiar de manera efectiva una señal emitida sin argumentos (por ejemplo, la señal de clic de un botón) a una señal con un argumento (su m
parámetro). De esta forma, no es necesario que envuelva la ranura en el QThread con partial()
.
El código está abajo. Creé una señal con un argumento (un int) llamado ''relé'' en la clase de Windows principal. La señal de clicked
botón está conectada a un método dentro de la clase de ventana principal, y este método tiene una línea de código que emite la señal personalizada que creé. Puedes extender este método ( relay_signal()
) para que el entero pase al QThread como m
(500 en este caso), ¡desde donde quieras!
Así que aquí está el código:
from functools import partial
from Queue import Queue
import math
import sys
import time
class Worker(QtCore.QObject):
termino = pyqtSignal()
def __init__(self, q=None, parent=None):
super(Worker, self).__init__(parent)
self.q = q
def run(self, m=30000):
print(''worker thread id: {}''.format(QtCore.QThread.currentThreadId()))
for x in xrange(m):
#y = math.sin(x)
y = x**2
#time.sleep(0.001) # Weird, plotting stops if this is not present...
self.q.put((x,y,y))
print(''Worker finished'')
self.termino.emit()
class MainWindow(QtGui.QWidget):
relay = pyqtSignal(int)
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.q = Queue()
self.termino = False
self.worker = Worker(self.q)
self.workerThread = None
self.btn = QtGui.QPushButton(''Start worker'')
self.pw = pg.PlotWidget(self)
pi = self.pw.getPlotItem()
pi.enableAutoRange(''x'', True)
pi.enableAutoRange(''y'', True)
self.ge1 = pi.plot(pen=''y'')
self.xs = []
self.ys = []
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.pw)
layout.addWidget(self.btn)
self.resize(400, 400)
def run(self):
self.workerThread = QtCore.QThread()
self.worker.termino.connect(self.setTermino)
self.worker.moveToThread(self.workerThread)
# moveToThread doesn''t work here
# self.btn.clicked.connect(partial(self.worker.run, 30000))
# moveToThread will work here
# assume def worker.run(self): instead of def worker.run(self, m=30000)
#self.btn.clicked.connect(self.worker.run)
self.relay.connect(self.worker.run)
self.btn.clicked.connect(self.relay_signal)
self.btn.clicked.connect(self.graficar)
self.workerThread.start()
self.show()
def relay_signal(self):
self.relay.emit(500)
def setTermino(self):
self.termino = True
def graficar(self):
if not self.q.empty():
e1,e2,ciclos = self.q.get()
self.xs.append(ciclos)
self.ys.append(e1)
self.ge1.setData(y=self.ys, x=self.xs)
if not self.termino or not self.q.empty():
QtCore.QTimer.singleShot(1, self.graficar)
if __name__ == ''__main__'':
app = QtGui.QApplication([])
window = MainWindow()
QtCore.QTimer.singleShot(0, window.run);
sys.exit(app.exec_())
También modifiqué el método graficar
para continuar el trazado (incluso después de que el hilo termine) si todavía hay datos en la cola. Creo que esta podría ser la razón por la que necesitabas el tiempo. time.sleep
en QThread, que ahora también se elimina.
También con respecto a sus comentarios en el código sobre dónde colocar moveToThread
, donde está ahora es correcto . Debería ser antes de la llamada que conecta la ranura QThread a una señal, y la razón de esto se discute en esta publicación de desbordamiento de pila: PyQt: Conexión de una señal a una ranura para iniciar una operación de fondo