threading python multithreading qt pyqt pyqt4

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