I'm following the python gui's tutorial for multithreading (which works as expected) and tried to adapt it to my case. However sometimes the thread runs as I expect while randomly the thread doesn't start or segfaults. What fault did I introduce in going from the example to my code?
The following is the minimum reproducible code to generate the issue.
import sys
import time
from PySide6.QtCore import QThreadPool, Slot, Signal,QObject,QRunnable
from PySide6.QtWidgets import QApplication,QMainWindow,QWidget,QVBoxLayout,QPushButton,QListWidget
class Device:
def long_function(self):
time.sleep(1)
return ['ciao','test']
class Ui_MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(800, 600)
self.setWindowTitle('Test1')
layout = QVBoxLayout()
self.button_start = QPushButton('Start')
layout.addWidget(self.button_start)
self.log = QListWidget()
layout.addWidget(self.log)
centralwidget = QWidget()
centralwidget.setLayout(layout)
self.setCentralWidget(centralwidget)
class WorkerSignals(QObject):
result = Signal(object)
class Worker(QRunnable):
def __init__(self,fn):
super().__init__()
self.fn = fn
self.signals = WorkerSignals()
@Slot()
def run(self):
res = self.fn()
self.signals.result.emit(res)
class Controller:
def __init__(self,ui,dev):
self.ui = ui
self.dev = dev
self.ui.button_start.clicked.connect(self.start)
self.threadpool = QThreadPool()
self.ui.show()
def start(self):
worker = Worker(lambda : self.dev.long_function())
worker.signals.result.connect(self.update)
self.threadpool.start(worker)
self.ui.log.addItem('Started')
@Slot()
def update(self,l):
self.ui.log.addItem('Ende d')
self.ui.log.addItems(l)
def main():
app = QApplication(sys.argv)
# views
mainwindow = Ui_MainWindow()
# models
dev = Device()
# controller
controller = Controller(mainwindow,dev)
sys.exit(app.exec())
if __name__ == '__main__':
main()
As pointed out in the comments to the question, the handling of QRunnable auto-deletion is buggy in PySide (see PYSIDE-2621). Ironically, if auto-delete is set to False in the example, the segfault will no longer occur, because PySide creates an internal reference when it shouldn't, and then never deletes it (resulting in a memory leak). However, this specific bug isn't the main cause of the problem in the example. When auto-delete is set to True (which is the default), the thread-pool takes ownership of its runnables and deletes them at the appropriate time, so it should never be necessary to maintain a separate Python reference as well.
The real cause of the problem seems to lie in how PySide manages certain signal connections. Normally, Qt will automatically disconnect signals when either the sender or receiver objects are deleted. However, this won't work with pure Python slots, since Qt obviously knows nothing about them. Thus PySide must use its own internal handling when cleaning up signal connections involving slots which aren't explicitly defined by a Qt class (or a user-defined subclass thereof).
With that in mind, the following minimal change will fix the example:
class Controller(QObject):
def __init__(self, ui, dev):
super().__init__()
... and note that it's not necessary to apply the Slot decorator to any methods of a QObject subclass that might be used as slots. PySide uses meta-class wizardry to automatically re-wrap these methods when the class statement is executed.
It's hard to say whether this is a PySide bug, or just a limitation. Similar problems can also happen in PyQt, where the essentially random ordering of Python garbage-collection can sometimes result in Qt trying to delete objects which are no longer there (for more details, see this answer). This means it's often wise to be cautious about using pure Python slots, unless you also take steps to explicitly disconnect them yourself when they're no longer needed. Generally speaking, both PySide and PyQt are pretty good at handling this kind of cleanup for you, but there's only so far they can go before it would start to have a serious impact on performance. Thus, there will always be a few edge cases left over that require special handling by the programmer, and the current case may just be one of them.
PS:
As further confirmation, the complete code example at the end of the tutorial mentioned in the question will also eventually segfault after changing one of the signals to something like this:
worker.signals.result.connect(lambda x: self.print_output(x))
PPS:
Just to be completely clear: it's the garbage-collection of the signals objects that causes the problem, not the worker objects. To prove this, the original example in the question can be "fixed" with this one-liner:
self.signals = WorkerSignals(QApplication.instance())
This will obviously leak memory, so it's not viable a solution - but it is a helpful debugging aid to show where the actual problem lies. The signal connections are between WorkerSignals.result and Controller.update, so the cleanest and simplest solution is to just ensure the latter is a Qt-style slot in the manner suggested above.