PySide2 - How to prevent crashing when running multi-threaded calculations then plotting the emitted results
-
I am writing a PySide2 app that plots the results to a certain calculation and trying to multithread the calculation to avoid locking up the GUI. I am trying to use QThreadPool to, upon interacting with options relating to the plot, run the calculation in a separate thread that returns the results via a signal to a callback method, which plots the results using matplotlib.
The problem is that when I change the selection of options in too quick (but not unreasonably quick) succession, the app crashes. This does not occur if the threading is removed.
I know a lot of problems are caused by having the plotting happen in the worker thread rather than the main thread so I believe I have made sure that the plotting only happens in the main thread.
I guess part of the problem is that I might be misunderstanding what is running where when using signals and slots. I have tried finding what thread is being used at different points in the code but can only use QThread.currentThread(), which returns the address and doesn't really help as QThread.currentThreadId() leads to this error: AttributeError: type object 'PySide2.QtCore.QThread' has no attribute 'currentThreadId'.
I have tried to isolate the behaviour by writing a minimal version of the app that crashes similarly, the majority of which I have put below. I have excluded the calculation as I am not sure if I can share it and I have replaced the plot options with a QListWidget with a few options. It requires more interaction to crash than the proper app that in some cases crashes after selecting just a few options in the space of a second or two but hopefully illustrates the point.
class MainWindow(QObject): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.main_window = QMainWindow() self.main_window.setCentralWidget(QWidget()) self.main_window.centralWidget().setLayout(QHBoxLayout()) self.setup_main_window() def setup_main_window(self): print(f'setup_main_window thread address: {QThread.currentThreadId()}') self.load_list() self.plot_figure = PlotFigure() self.canvas = FigureCanvas(self.plot_figure) self.plot_figure.plot(update=False) self.main_window.centralWidget().layout().addWidget(self.canvas) def load_list(self): self.order_list = QListWidget(self.main_window) self.list_items = [ QListWidgetItem('1', self.order_list), QListWidgetItem('2', self.order_list), QListWidgetItem('3', self.order_list), QListWidgetItem('4', self.order_list), ] self.order_list.itemClicked.connect(self.order_list_item_changed) self.main_window.centralWidget().layout().addWidget(self.order_list) def order_list_item_changed(self): print(f'order_list_item_changed thread address: {QThread.currentThreadId()}') self.plot_figure.plot() def show(self): if self.main_window is not None: self.main_window.show() class PlotFigure(Figure): def __init__(self): super().__init__() def plot(self): print(f'plot thread address: {QThread.currentThreadId()}') print(f'update: {update}') print(f'connecting signals') worker = Worker(self.calc) #worker.signals.close.connect(self.set_end_calc) worker.signals.finished.connect(self.plot_result) print(f'threads: {QThreadPool.globalInstance().activeThreadCount()}') QThreadPool.globalInstance().start(worker) def plot_result(self, m, xs, ys): print(f'plot_result thread address: {QThread.currentThreadId()}') print('plotting') fig = self.canvas.figure fig.clear() self.axis = fig.add_subplot(111) self.image = self.axis.imshow(m, origin='lower', aspect='auto', cmap=matplotlib.cm.get_cmap('inferno'), interpolation='bilinear', extent=(xs[0], xs[-1], ys[0], ys[-1]) ) self.canvas.draw()
class WorkerSignals(QtCore.QObject): close = QtCore.Signal(bool) start = QtCore.Signal(bool) finished = QtCore.Signal(list, list, list) class Worker(QtCore.QRunnable): def __init__(self, worker_method): super(Worker, self).__init__() self.signals = WorkerSignals() self.worker_method = worker_method def run(self): self.signals.close.emit(True) print('close signal sent') m, xs, ys = self.worker_method() print('calc done') self.signals.finished.emit(m, xs, ys)
I should be able to select new options (click around the list widget), start a new thread from the threadpool, which runs the calculation and sends back the results to be plotted. When too many options are selected in a short time, the app crashes. This doesn't happen when everything happens in the main thread.
Can anyone tell me why the app might be crashing and offer solutions to fix the crash?
-
Hi,
What does the crash tell you ?
-
Maybe crash is the wrong term, it just becomes unresponsive and exits the program. I previously had it print out messages at each point and the last message was mostly when it was connecting the worker signals but sometimes after it had finished the calculation and was sending the signal with it's results.
-
The first thing you should do is to make Worker inherit both QObject and QRunnable. QObject must come first. Making it a separated class and calling signals on it is wrong. Signals should be emitted from within the class that defines them.
Another thing is that you don't set your worker to auto delete so you will end up eating lots of memories depending on how many times plot is called.