Home > Back-end >  PyQt5: connecting signals to lambda functions used with QRunnable in a worker thread
PyQt5: connecting signals to lambda functions used with QRunnable in a worker thread

Time:07-25

I have a GUI made with PyQt5, and there is a start-button on it. When the start-button is clicked, the program will start to count from 1 to 5 and then show a pop-up window to ask if the task should be interrupted. If the user clicks the "Yes" button on the pop-up window, a signal will be emitted which will instigate execution of a function (called interrupt_test) in the worker class because of a connection I create in the main program (the module call_main_window.py). Then the task will be stopped.

I find that every time when I click the start-button, the function interrupt_test in the worker class will be executed one more time than the last time I clicked the start-button. I know this because the message "The 'interrupt_test' function is executed." will be printed every time when the function interrupt_test is called. And I found that this message shows up repeatedly, except for the first time I click the start-button.

Below is my code in the module called call_main_window.py:

from PyQt5 import QtCore, QtWidgets, QtGui
from main_window import Ui_Form
import time
import sys

class Stream(QtCore.QObject):

  newText = QtCore.pyqtSignal(str)

  def write(self, data: str):
    self.newText.emit(data)

class CustomSignal(QtCore.QObject):

    toInterrupt = QtCore.pyqtSignal() # line 15
#    toInterrupt = QtCore.pyqtSignal(bool)
    toCallMsgBox = QtCore.pyqtSignal()

class Worker(QtCore.QRunnable):

  def __init__(self, mutex: QtCore.QMutex, cond: QtCore.QWaitCondition):
    super(Worker, self).__init__()
    self.customSignal = CustomSignal()
    self.mutex = mutex
    self.cond = cond
    self.shouldInterruptTest = False

  def start(self):
    for i in range(1, 11):
      if i == 6:
        self.mutex.lock()
        self.customSignal.toCallMsgBox.emit()
        self.cond.wait(self.mutex)
        self.mutex.unlock()

      if self.shouldInterruptTest:
        sys.stdout.write(f'The test is interrupted.\n')
        break
      else:
        sys.stdout.write(f'{i}\n')
        time.sleep(1)

  def interrupt_test(self, shouldInterruptTest: bool):
    self.shouldInterruptTest = shouldInterruptTest
    sys.stdout.write(f'The \'interrupt_test\' function is executed.\n')

class MyMainWindow(QtWidgets.QWidget, Ui_Form):

  def __init__(self, parent = None):
    super(MyMainWindow, self).__init__(parent)
    self.setupUi(self)
    self.mutex = QtCore.QMutex()
    self.cond = QtCore.QWaitCondition()
    self.customSignal = CustomSignal()
    self.pushButtonStart.clicked.connect(self.start_working)
    self.destroyed.connect(sys.exit)
    sys.stdout = Stream(newText = self.update_text)

  def start_working(self):
    worker = Worker(mutex = self.mutex, cond = self.cond)
    worker.customSignal.toCallMsgBox.connect(self.call_msg_box)
    self.customSignal.toInterrupt.connect(lambda: worker.interrupt_test(True)) # line 62
#    self.customSignal.toInterrupt.connect(worker.interrupt_test)
    self.threadPool = QtCore.QThreadPool()
    self.threadPool.start(worker.start)

  def call_msg_box(self):
    sys.stdout.write('call_msg_box is called\n')
    msg = QtWidgets.QMessageBox()
    msg.setIcon(QtWidgets.QMessageBox.Question)
    msg.setText('Do you want to stop the test?')
    msg.setWindowTitle('Info')
    msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)

    self.mutex.lock()
    reply = msg.exec_()
    if reply == QtWidgets.QMessageBox.Yes:
        self.customSignal.toInterrupt.emit()
#        self.customSignal.toInterrupt.emit(True)
    self.cond.wakeAll()
    self.mutex.unlock()

  def update_text(self, data: str):
    cursor = self.plainTextEdit.textCursor()
    cursor.movePosition(QtGui.QTextCursor.End)
    cursor.insertText(data)
    self.plainTextEdit.setTextCursor(cursor)
    self.plainTextEdit.ensureCursorVisible()

if __name__ == '__main__':
  app = QtWidgets.QApplication(sys.argv)
  window = MyMainWindow()
  window.show()
  sys.exit(app.exec_())

Below is the code for GUI in another module called main_window.py:

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(300, 300)
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")

        self.plainTextEdit = QtWidgets.QPlainTextEdit(Form)
        self.plainTextEdit.setObjectName("plainTextEdit")
        self.gridLayout.addWidget(self.plainTextEdit, 0, 0, 1, 1)
        spacerItem = QtWidgets.QSpacerItem(500, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)

        self.pushButtonStart = QtWidgets.QPushButton(Form)
        self.pushButtonStart.setObjectName("pushButtonStart")
        self.gridLayout.addWidget(self.pushButtonStart, 1, 0, 1, 1)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.pushButtonStart.setText(_translate("Form", "Start"))

I think the connection in line 62 of the call_main_window.py module is not automatically disconnected when the worker thread is finished. This problem can be fixed if I mark line 15 and 62 of the call_main_window.py module and unmark line 16 and 63. But I don't understand how line 15 and 62 will instigate the problem I come across. Can anyone kindly explain it to me?

CodePudding user response:

The problem is not caused by the lambda as such, but by the closure it creates. Any callable that forms a closure may exhibit the same behaviour, depending on the specific type of callable it is. For details of exactly how and when this happens, see here:

Qt knows nothing about Python, so it all it can do is remove C references to objects it has ownership of. If this doesn't reduce the total reference count to zero, the object will be kept alive. This is something you must always be aware of if you intend to clean up objects during the lifetime of your program.

To fix your example, you can explicitly disconnect the signals like this:

class MyMainWindow(QtWidgets.QWidget, Ui_Form):
  ...
  def start_working(self):
    try:
      self.customSignal.toInterrupt.disconnect()
    except TypeError:
      pass
    worker = Worker(mutex = self.mutex, cond = self.cond)
    worker.customSignal.toCallMsgBox.connect(self.call_msg_box)
    self.customSignal.toInterrupt.connect(lambda: worker.interrupt_test(True))
    self.threadPool = QtCore.QThreadPool()
    self.threadPool.start(worker.start)
  • Related