PyQT5 Threading с QRunnable - правильный способ отправки двунаправленных обратных вызовов - PullRequest
3 голосов
/ 06 мая 2020

Из учебника я видел, что сигналы и слоты могут использоваться для выполнения обратных вызовов из рабочего потока в основной поток GUI, но я не уверен, как установить sh двунаправленная связь с использованием сигналов и слотов. Вот с чем я работаю:

class RespondedToWorkerSignals(QObject):
    callback_from_worker = pyqtSignal()

class RespondedToWorker(QRunnable):
    def __init__(self, func, *args, **kwargs):
        super(RespondedToWorker, self).__init__()
        self._func = func
        self.args = args
        self.kwargs = kwargs
        self.signals = RespondedToWorkerSignals()
        self.kwargs['signal'] = self.signals.callback_from_worker
        print("Created a responded-to worker")

    @pyqtSlot()
    def run(self):
        self._func(*self.args, **self.kwargs)

    @pyqtSlot()
    def acknowledge_callback_in_worker(self):
        print("Acknowledged Callback in Worker")

class MainWindow(QMainWindow):

    # Signal meant to connect to a slot present within a worker
    mainthread_callback_to_worker = pyqtSignal()

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        # Quick UI setup
        w, lay = QWidget(), QVBoxLayout()
        w.setLayout(lay)
        self.setCentralWidget(w)
        self.timer_label = QLabel("Timer Label")
        lay.addWidget(self.timer_label)
        self.btn_thread_example = QPushButton("Push Me")
        self.btn_thread_example.pressed.connect(self.thread_example)
        lay.addWidget(self.btn_thread_example)
        self.threadpool = QThreadPool()
        self.show()

        # Set up QTimer to continue in the background to help demonstrate threading advantage
        self.counter = 0
        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    @pyqtSlot()
    def do_something(self, signal):
        # signal argument will be the callback_from_worker and it will emit to acknowledge_callback_in_mainthread
        print("do_something is sleeping briefly. Try to see if you get a locked widget...")
        time.sleep(7)
        signal.emit()

    @pyqtSlot()
    def acknowledge_callback_in_mainthread_and_respond(self):
        # this function should respond to callback_from_worker and emit a response
        print("Acknowledged Callback in Main")
        self.mainthread_callback_to_worker.emit()

    def thread_example(self):
        print("Beginning thread example")
        worker = RespondedToWorker(self.do_something)
        worker.signals.callback_from_worker.connect(self.acknowledge_callback_in_mainthread_and_respond)
    # self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash

    def recurring_timer(self):
        self.counter += 1
        self.timer_label.setText(f"Counter: {self.counter}")

if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = MainWindow()
    app.setStyle("Fusion")
    win.show()
    sys.exit(app.exec())

В настоящий момент скрипт может создать второй поток и отправить сигнал основному GUI. Я бы хотел, чтобы GUI отправил ответный сигнал обратно рабочему потоку. Я также не уверен, почему подключение основного / gui сигнала mainthread_callback_to_worker вызывает ошибку sh (см. Закомментированную строку).

Я понимаю, что один обходной путь был бы для do_something чтобы вернуть какое-то значение, а затем использовать его внутри рабочего как «подтверждение». Но я хотел бы знать решение, используя сигналы и слоты, если возможно.

1 Ответ

2 голосов
/ 06 мая 2020

Чтобы понять причину ошибки, вы должны запустить код в терминале, и вы получите следующее сообщение об ошибке:

QObject::connect: Cannot connect MainWindow::mainthread_callback_to_worker() to (nullptr)::acknowledge_callback_in_worker()
Traceback (most recent call last):
  File "main.py", line 72, in thread_example
    self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
TypeError: connect() failed between MainWindow.mainthread_callback_to_worker[] and acknowledge_callback_in_worker()
Aborted (core dumped)

Причина ошибки - злоупотребление pyqtSlot декоратор, поскольку он должен использоваться только в методах QObject, но QRunnable не вызывает этого исключения, кроме того, в объектах, отличных от QObject, он не использует никаких преимуществ, поэтому декоратор в методе run() не имеет смысла.

С другой стороны, QRunnable - это только интерфейс, который живет в основном потоке, и только метод запуска выполняется в другом потоке, поэтому QRunnable не может быть рабочим, поскольку этот тип цели должен выполнять свои методы во вторичном потоке.

Таким образом, с вышеуказанным QRunnable не подходит, поэтому для вашей цели я рекомендую использовать QObject, который живет во вторичном потоке, и вызывать методы.

import sys
import time

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer, QThread
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QLabel,
    QPushButton,
)


class Worker(QObject):
    callback_from_worker = pyqtSignal()

    def __init__(self, func, *args, **kwargs):
        super(Worker, self).__init__()
        self._func = func
        self.args = args
        self.kwargs = kwargs
        self.kwargs["signal"] = self.callback_from_worker

    def start_task(self):
        QTimer.singleShot(0, self.task)

    @pyqtSlot()
    def task(self):
        self._func(*self.args, **self.kwargs)

    @pyqtSlot()
    def acknowledge_callback_in_worker(self):
        print("Acknowledged Callback in Worker")
        print(threading.current_thread())


class MainWindow(QMainWindow):
    mainthread_callback_to_worker = pyqtSignal()

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        w, lay = QWidget(), QVBoxLayout()
        w.setLayout(lay)
        self.setCentralWidget(w)
        self.timer_label = QLabel("Timer Label")
        lay.addWidget(self.timer_label)
        self.btn_thread_example = QPushButton("Push Me")
        self.btn_thread_example.pressed.connect(self.thread_example)
        lay.addWidget(self.btn_thread_example)

        self.counter = 0
        self.timer = QTimer(interval=1000, timeout=self.recurring_timer)
        self.timer.start()

        self._worker = Worker(self.do_something)
        self._worker.callback_from_worker.connect(
            self.acknowledge_callback_in_mainthread_and_respond
        )

        self.worker_thread = QThread(self)
        self.worker_thread.start()
        self._worker.moveToThread(self.worker_thread)

    @pyqtSlot()
    def do_something(self, signal):
        print(
            "do_something is sleeping briefly. Try to see if you get a locked widget..."
        )
        time.sleep(7)
        signal.emit()

    @pyqtSlot()
    def acknowledge_callback_in_mainthread_and_respond(self):
        print("Acknowledged Callback in Main")
        self.mainthread_callback_to_worker.emit()

    def thread_example(self):
        print("Beginning thread example")
        self._worker.start_task()

    def recurring_timer(self):
        self.counter += 1
        self.timer_label.setText(f"Counter: {self.counter}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    app.setStyle("Fusion")
    win.show()
    ret = app.exec_()
    win.worker_thread.quit()
    win.worker_thread.wait()
    sys.exit(ret)
...