Issue
I was messing around with an application that has a save/load function. Both reading the saved file and loading all the components takes enough time to halt the program for a bit. I found out that while I can read the saved file, I should avoid creating any QObjects inside the thread (the objects needs signals, hence QObject). To fix this, I emit the contents of the saved file and handle creating objects outside, but since the creation of objects also takes some time, I am not ready to give up on the idea.
I have re-created a minimal working example, that shows the problem. The objects are created correctly either way - normally or threaded - however, the signals are not emitted (or emitting into the void in a thread that has now been garbage collected). I use(d) this pattern in my application, except I emit the stuff I need from the thread and to stuff, instead of doing stuff inside the thread.
If this truly is just a bad idea and against good programming practice, just tell me and I will have to adjust or live with a stuttering GUI. The reason why I am keen on avoiding these stutters is because I utilize a spinning loader-gif on heavy operations and it keeps running it I perform the heavy stuff in a separate thread (come to think of it, maybe the gif can run in its own thread instead...?)
from PySide6 import QtCore, QtWidgets
class SomeParentContainer(QtCore.QObject):
some_signal = QtCore.Signal(str)
def __init__(self):
self.container = list()
super().__init__()
class SomeObjectWithSignals(QtCore.QObject):
some_signal = QtCore.Signal(str)
def __init__(self, parent, dummy_id):
self.parent = parent
self.dummy_id = dummy_id
super().__init__()
self.some_signal.connect(lambda dummy_id: self.parent.some_signal.emit(dummy_id))
def fake_emit(self):
self.some_signal.emit(self.dummy_id)
container_obj = SomeParentContainer()
class SomeMainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
container_obj.some_signal.connect(lambda dummy_id: print(f"some_signal emitted from child: {dummy_id}"))
self.main_widget = QtWidgets.QWidget()
self.setCentralWidget(self.main_widget)
self.layout = QtWidgets.QVBoxLayout()
btn = QtWidgets.QPushButton()
btn.clicked.connect(self.btn1)
btn.setText("Add normally")
self.layout.addWidget(btn)
btn = QtWidgets.QPushButton()
btn.clicked.connect(self.btn2)
btn.setText("Add threaded")
self.layout.addWidget(btn)
btn = QtWidgets.QPushButton()
btn.clicked.connect(self.btn3)
btn.setText("Emit signals")
self.layout.addWidget(btn)
self.main_widget.setLayout(self.layout)
def btn1(self):
container_obj.container.append(SomeObjectWithSignals(container_obj, "test1"))
def btn2(self):
class SomeThreadedAction(QtCore.QThread):
finished = QtCore.Signal()
def run(self):
container_obj.container.append(SomeObjectWithSignals(container_obj, "test2"))
self.thread = SomeThreadedAction()
self.thread.finished.connect(lambda: print("thread finished"))
self.thread.start()
def btn3(self):
for objects in container_obj.container:
print("Should be emitting something next:")
objects.fake_emit()
print("-------------------------")
app = QtWidgets.QApplication()
form = SomeMainWindow()
form.show()
app.exec()
Solution
I don't recommend sharing QObject
instances between threads because it will make your code unnecessarily complex. It will be much more simple, if you transfer a result of pure Python objects from the worker thread into the main thread, and create QObject
instances from the result in the main thread.
But if you insist on transfering QObject
instances, you have to care about these points.
You should change the thread affinity of a
QObject
instance. See Per-thread Event Loop.You should give a correct hint on a signal receiver when you call the
connect()
. If you do not specify a slot of aQObject
instance, a dummy receiver with an affinity to the calling thread will be created.You should keep a
QObject
instance alive until the receiving thread owns it.
So, you can do like the following.
...
class SomeParentContainer(QtCore.QObject):
...
def emit_some_signal(self, dummy_id):
self.some_signal.emit(dummy_id)
class SomeObjectWithSignals(QtCore.QObject):
def __init__(self, parent, dummy_id):
...
#self.some_signal.connect(lambda dummy_id: self.parent.some_signal.emit(dummy_id))
self.moveToThread(parent.thread()) # The point #1.
self.some_signal.connect(parent.emit_some_signal) # The point #2.
self.setParent(parent) # The point #3.
...
class SomeMainWindow(QtWidgets.QMainWindow):
...
def btn2(self):
class SomeThreadedAction(QtCore.QThread):
#finished = QtCore.Signal() # It's bad to override the QThread.finished.
obj_created = QtCore.Signal(QtCore.QObject)
def run(self):
#container_obj.container.append(SomeObjectWithSignals(container_obj, "test2"))
# It's bad to update the state of an object
# owned by the main thread in a worker thread.
self.obj_created.emit(SomeObjectWithSignals(container_obj, "test2"))
self.thread = SomeThreadedAction()
self.thread.finished.connect(lambda: print("thread finished"))
self.thread.obj_created.connect(container_obj.container.append)
self.thread.start()
...
Answered By - relent95
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.