I am troubleshooting a threading application and I noticed the following behaviour: in PySide6, I would like to connect to both slots and non-slot methods, which accoridng to documentation should work. However it does not work as expected. I am not sure if it is a bug or I am doing something incorrectly?
- In PySide6, if a
QObject
is first connected and then moved into a new thread inside a slot, the slots of theQObject
will not execute in the new thread when invoked by a signal (with aQueuedConnection
), however the methods that are not slots will. If the object is first moved and then connected up, everything is ok. - In PyQt5, the situation is reversed. Methods that are not slots do not follow the
QObject
to a new thread but slots do. My understanding is that this is correct, since in PyQt5 the slot declaration was obligatory for QueuedConnection to work.
I built a minimal application that illustrates the problem. The buttons are clicked in sequence to move the Dummy
object into a new thread and then trying to call do_work
method/do_work_slot
slot with buttons via different mechanisms and inspecting the thread in which the method is executed.
Clicking the buttons in sequence from top to bottom in PyQt5 gives log:
MainThread:move_to_thread: object thread: Main Thread, execution thread: Main Thread
MainThread:do_work: object thread: new thread, execution thread: Main Thread
MainThread:do_work: object thread: new thread, execution thread: Main Thread
Dummy-1:do_work_slot: object thread: new thread, execution thread: new thread
Dummy-1:do_work_slot: object thread: new thread, execution thread: new thread
whereas in PySide6 it is:
MainThread:move_to_thread: object thread: Main Thread, execution thread: Main Thread
MainThread:do_work: object thread: new thread, execution thread: Main Thread
Dummy-1:do_work: object thread: new thread, execution thread: new thread
MainThread:do_work_slot: object thread: new thread, execution thread: Main Thread
Dummy-1:do_work_slot: object thread: new thread, execution thread: new thread
Note that the thread affinity of the object is correct for both packages.
The behaviour I expected on PySide6 is (I added extra stars for highlight of the relevant bits)
MainThread:move_to_thread: object thread: Main Thread, execution thread: Main Thread
MainThread:do_work: object thread: new thread, execution thread: Main Thread
Dummy-1:do_work: object thread: new thread, execution thread: **new thread**
**Dummy-1**:do_work_slot: object thread: new thread, execution thread: **new thread**
Dummy-1:do_work_slot: object thread: new thread, execution thread: new thread
I attach the application.
import logging
import sys, os
os.environ["QT_API"] = "pyqt5" # ""PySide6"
from qtpy import QtCore, QtWidgets
logging.basicConfig(format="%(asctime)s:%(levelname)s:%(threadName)s:%(name)s: %(message)s", level=logging.DEBUG)
msg_template = "object thread: {thread1}, execution thread: {thread2}"
class Dummy(QtCore.QObject):
sigMoveToThread = QtCore.Signal()
sigMoveToMain = QtCore.Signal()
sigRequestWork = QtCore.Signal()
def __init__(self, parent=None):
QtCore.QObject.__init__(self, parent=parent)
self.sigMoveToThread.connect(self.move_to_thread)
self.sigMoveToMain.connect(self.move_to_main)
self.sigRequestWork.connect(self.do_work_slot)
def do_work(self):
logging.info(msg_template.format(
thread1=self.thread().objectName(),
thread2=QtCore.QThread.currentThread().objectName()
))
@QtCore.Slot()
def do_work_slot(self):
logging.info(msg_template.format(
thread1=self.thread().objectName(),
thread2=QtCore.QThread.currentThread().objectName()
))
@QtCore.Slot()
def move_to_thread(self):
logging.info(msg_template.format(
thread1=self.thread().objectName(),
thread2=QtCore.QThread.currentThread().objectName()
))
new_thread = QtCore.QThread()
new_thread.setObjectName("new thread")
self.moveToThread(new_thread)
new_thread.start()
self.t = new_thread # to avoid garbage collector
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = QtWidgets.QMainWindow()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
QtCore.QThread.currentThread().setObjectName("Main Thread")
dummy = Dummy()
# moving to thread at THIS stage as below would result in correct moving
# of both Slot and non-Slot connection in PySide6
# thread = QtCore.QThread()
# dummy.moveToThread(thread)
button = QtWidgets.QPushButton("Click (Direct connection, not slot)")
button.clicked.connect(dummy.do_work, QtCore.Qt.DirectConnection)
layout.addWidget(button)
button = QtWidgets.QPushButton("Click (Queued connection, not slot)")
button.clicked.connect(dummy.do_work, QtCore.Qt.QueuedConnection)
layout.addWidget(button)
button = QtWidgets.QPushButton("Click (Queued connection, slot)")
button.clicked.connect(dummy.do_work_slot, QtCore.Qt.QueuedConnection)
layout.addWidget(button)
button = QtWidgets.QPushButton("Click (sigRequestWork)")
button.clicked.connect(dummy.sigRequestWork.emit)
layout.addWidget(button)
button = QtWidgets.QPushButton("Move to thread (move_to_thread slot)")
button.clicked.connect(dummy.move_to_thread, QtCore.Qt.QueuedConnection)
layout.addWidget(button)
button = QtWidgets.QPushButton("Move to thread (sigMoveToThread)")
button.clicked.connect(dummy.sigMoveToThread.emit, QtCore.Qt.QueuedConnection)
layout.addWidget(button)
widget.setLayout(layout)
window.setCentralWidget(widget)
window.show()
sys.exit(app.exec())
Apparently upgrading from PySide6-6.6.1
to PySide-6.7.1
helped.
I also checked in PyQt6-6.7.1
and it works as expected.