I have an event-producer thread, creating Event instances in that Producer
thread and passing them to the GUI thread as signal argument, after moving the object to the GUI thread (in real-life code, so the object can be added into a Model for displaying).
In a first attempts described here:
- PyQt5 succeeds in getting the object passed, but it is seen in the slot as a
QObject
not an Event - PySide2 just segfaults
#! /usr/bin/env python3
import sys, threading
if True:
from PySide2 import QtCore
from PySide2.QtCore import QObject
QtSignal = QtCore.Signal
else:
from PyQt5 import QtCore
from PyQt5.QtCore import QObject
QtSignal = QtCore.pyqtSignal
def print_thread(ctx):
print(ctx, "in", threading.current_thread().name)
class Event(QObject):
def __init__(self, name: str):
super().__init__()
self.name = name
class Producer(QtCore.QThread):
woo = QtSignal(Event)
def run(self):
print_thread("Producer.run")
evt = Event("bar")
evt.moveToThread(QtCore.QCoreApplication.instance().thread())
print("emitting", evt)
self.woo.emit(evt)
class Listener(QObject):
def handle_event(self, event):
print_thread("Listener.handle_event")
print("got", event)
assert type(event) is Event
exit(0)
app = QtCore.QCoreApplication(sys.argv)
l = Listener()
p = Producer()
p.woo.connect(l.handle_event)
p.start()
sys.exit(app.exec_())
Both show the same typing issue under PyQt5, caught by my assert
:
Producer.run in Dummy-1
emitting <__main__.Event object at 0x7af7ad95b790>
Listener.handle_event in MainThread
got <PyQt5.QtCore.QObject object at 0x7af7ad95b790>
Traceback (most recent call last):
File "/home/user/soft/bt2viz/testsig.py", line 35, in handle_event
assert type(event) is Event
AssertionError
Aborted
… and under PySide2:
$ gdb --args python3 testsig.py
GNU gdb (Debian 10.1-1.7) 10.1.90.20210103-git
...
(gdb) r
Starting program: /usr/bin/python3 testsig.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff3b4f700 (LWP 6190)]
Producer.run in Dummy-1
emitting <__main__.Event(0x7fffec005740) at 0x7ffff75577c0>
[Thread 0x7ffff3b4f700 (LWP 6190) exited]
Thread 1 "python3" received signal SIGSEGV, Segmentation fault.
0x00007fffec005760 in ?? ()
(gdb) bt
#0 0x00007fffec005760 in ?? ()
#1 0x00007ffff6add361 in QObject::property (this=this@entry=0x7fffec005740, name=name@entry=0x7ffff6d6e5f0 <PySide::invalidatePropertyName> "_PySideInvalidatePtr")
at kernel/qobject.cpp:4086
#2 0x00007ffff6d6ae17 in PySide::getWrapperForQObject (cppSelf=0x7fffec005740, sbk_type=0xbbb910) at ./sources/pyside2/libpyside/pyside.cpp:441
...
As it turns out, and as hinted by https://stackoverflow.com/a/12363609/6285023, what happens is that the Event object gets destroyed when getting out of scope — and that eg. just keeping a ref to it within the Producer object avoids the issue.
But that is wasteful (keeping a list of a large number of events in one thread, when they are already referenced in the other thread’s data model), and does not feel very pythonic: the other thread gets what looks like a ref to the object — at least it feels like a standard ref, except that it does not seem to be included in the usual refcounting, and gets deleted too early.
This looks like a C++ism not properly translating into Python, and could be considered as a PyQt/PySide bug, right ?
Is there a more proper way to achieve the same result, or do we have to live with the above workaround ?
2
Answers
The answer to my problem is as described by @ekhumoro in his comment to my question: my
Event
class does not need to inheritQObject
:OTOH @eyllanesc's description of why the problem happens is really accurate, and does allow to solve the more general problem, should the need to pass a QObject out of its C++ scope.
There are differences between memory management between C++ and python, in the first can use dynamic memory (typically pointers) but in the second use references. So each library (PyQt and PySide) decides the behavior it wants to give it since Qt doesn’t signal anything about it. It seems that PyQt has a reference to the python object if the object doesn’t have a QObject as its parent when sent through signals, and PySide doesn’t. So maybe you could ask PySide for that feature for discussion and if the change is accepted then in future releases it can be pushed into PySide6 (since it has the same behavior too).
On the other hand, a workaround in this case is to make the QObject have a parent, and in this case it can be an object that lives on the same thread it was moved to, and then remove it: