Unexpected freezing of graph while GUI still works and without error message | Pyside6 and pyqtgraph with multi-threading (Python 3.11.4)

huangapple go评论65阅读模式
英文:

Unexpected freezing of graph while GUI still works and without error message | Pyside6 and pyqtgraph with multi-threading (Python 3.11.4)

问题

目标:
我正在创建一个用于在图表上显示传感器输入的GUI。为了使这个过程可重复和直观,我用一个简单的正弦函数替代了传感器的输入。程序运行时会显示一个窗口,上面有两个按钮,一个用于启动,一个用于暂停/继续。

方法:
GUI是使用PySide6构建的,图形小部件是使用pyqtgraph创建的。数据的生成和绘制是在一个单独的“Worker”线程中完成的,而其他所有工作(按按钮、设置小部件等)都在主线程中完成。

问题:
最终,图表会突然冻结。这可能需要10-20分钟。有没有人知道可能出了什么问题?

可重现性:如果与GUI进行交互,如按按钮,图表可能会更快地冻结。在RaspberryPi 4b(Python 3.9)上测试时,冻结速度更快。在终端中打印数据表明,它一直在更新数据集,但图表没有相应地更新。我必须关闭应用程序,然后再次运行代码才能再次工作。关闭应用程序时不会报告错误。

以下是用于信号和“Worker”线程的两个类:

import sys
import traceback
import numpy as np
import time
from PySide6.QtCore import (Qt, QObject, QRunnable, QThreadPool, Slot, Signal)
from PySide6.QtWidgets import (QMainWindow, QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QGraphicsView, QStackedLayout)
import pyqtgraph as pg
from qtrangeslider._labeled import QLabeledSlider

###___用于两个线程之间通信的信号___###
class WorkerSignals(QObject):
    error = Signal(tuple)

###___Worker线程,与主线程分开运行___###
class Worker(QRunnable):
    def __init__(self, interval_range, env_curve, env_plot):
        super().__init__()

        # 设置参数和信号
        self.signals = WorkerSignals()
        self.interval_range = interval_range
        self.env_curve = env_curve
        self.env_plot = env_plot

        # 进一步设置绘图
        self.start = self.interval_range[0]
        self.length = self.interval_range[1] - self.interval_range[0]
        self.num_depths = 2000  # 长度轴上的点数
        self.depths = np.linspace(self.start, self.start + self.length, self.num_depths)

        self.pause = False
        self.stop = False
        self.go = True
        self.n = 0

    # 为run函数指定一个Slot,使其能够在单独的线程上运行
    @Slot()
    def run(self):

        while self.go == True:
            self.n += 1

            # 通过切换self.pause(下面在此线程中的函数)来冻结和启动绘图
            while self.pause == True:
                time.sleep(0.01)

            # 这个try块生成数据并更新绘图,一遍又一遍
            try:
                data = np.sin(2 * np.pi * (self.depths + 0.001 * self.n))
                self.env_curve.setData(self.depths, data)
                self.env_plot.setYRange(-1, 1)

            # 如果发生错误,这个块将帮助在终端中显示错误
            except:
                traceback.print_exc()
                exctype, value = sys.exc_info()[:2]
                self.signals.error.emit((exctype, value, traceback.format_exc()))
                self.go = False

    # 如果“启动/停止”按钮(在主线程中指定)被切换,将访问此函数
    def start_stop_worker(self):
        if self.pause == False:
            self.pause = True
        elif self.pause == True:
            self.pause = False

以下是构建GUI的主线程:

###___主线程___###
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # 窗口名称和尺寸
        self.setWindowTitle("My App")
        self.setFixedWidth(800)
        self.setFixedHeight(480)

        # 绘图网格外观
        pg.setConfigOption("background", "w")
        pg.setConfigOption("foreground", "k")
        pg.setConfigOptions(antialias=True)

        self.interval_range = [0, 2 * np.pi]

        central_widget = QWidget()
        plot_lay = QVBoxLayout(central_widget)
        self.win = pg.GraphicsLayoutWidget()
        plot_lay.addWidget(self.win)
        btn_1 = QPushButton('Start')
        btn_2 = QPushButton('Pause/Resume')
        btn_lay = QHBoxLayout(central_widget)
        plot_lay.addWidget(btn_1)
        plot_lay.addWidget(btn_2)
        plot_lay.addLayout(btn_lay)

        self.env_plot = self.win.addPlot()
        self.env_plot.showGrid(x=True, y=True)
        self.env_plot.setLabel("bottom", "Depth (m)")
        self.env_plot.setLabel("left", "Amplitude")
        self.env_curve = self.env_plot.plot(pen=pg.mkPen("k", width=2))

        # 每个按钮与一个或多个函数的连接
        btn_1.pressed.connect(self.start_live_plot)
        btn_2.pressed.connect(self.start_stop)

        # 最后创建并设置一个包含我们创建的所有内容的中央小部件
        central_widget.setLayout(plot_lay)
        self.setCentralWidget(central_widget)

    # 转到实时绘图选项卡并启动Worker线程
    def start_live_plot(self):
        self.threadpool = QThreadPool()
        self.worker = Worker(self.interval_range, self.env_curve, self.env_plot)
        self.threadpool.start(self.worker)

    # 访问Worker线程中的启动停止函数
    def start_stop(self):
        self.worker.start_stop_worker()

app = QApplication([])
window = MainWindow()
window.show()
app.exec()

请注意,我已经删除了部分代码以简化问题。如果需要进一步的帮助,请告诉我。

英文:

Goal:
I'm creating a GUI that will be used for displaying input from a sensor on a graph. To make this repeatable and intuitive I've replaced any input from the sensor with a simple sinus function. The program, when run, displays one window with two buttons. One for starting and one for paus/resume.

Method:
The GUI is built using PySide6 and the graphical widget is created using pyqtgraph. The generation and plotting of data is done in a separate 'Worker'-thread while everything else (pressing buttons, setting up widgets, etc) is done in the main thread.

Problem:
Eventually the graph will suddenly freeze. This might take 10-20 min. Does anyone have any idea as to what could be wrong?

Reproducibility: The graph might freeze faster if the GUI is interacted with. Like pressing buttons. Testing this on a RaspberryPi 4b (Python 3.9) gave even faster freezes. Printing the data in the terminal reveals that it keeps updating the data set but it's the graph that isn't updating accordingly. I have to close the application and run the code again for it to work again. When closing the application no error is reported.

Here are the two classes for signals and the 'Worker'-thread:

import sys
import traceback
import numpy as np
import time
from PySide6.QtCore import (Qt, QObject, QRunnable, QThreadPool, Slot, Signal)
from PySide6.QtWidgets import (QMainWindow, QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QGraphicsView, QStackedLayout)
import pyqtgraph as pg
from qtrangeslider._labeled import QLabeledSlider
###___Signals for communication between the two threads___###
class WorkerSignals(QObject):
error = Signal(tuple)
###___The Worker-Thread, running seperately from the Main-Thread___###
class Worker(QRunnable):
def __init__(self, interval_range, env_curve, env_plot):
super().__init__()
#Setting arguments and signals#
self.signals = WorkerSignals()
self.interval_range = interval_range
self.env_curve = env_curve
self.env_plot = env_plot
#Further setting to the plotting#
self.start = self.interval_range[0]
self.length = self.interval_range[1]-self.interval_range[0]  
self.num_depths = 2000       #number of points on the length axis#
self.depths = np.linspace(self.start, self.start + self.length, self.num_depths)
self.pause = False
self.stop = False
self.go = True
self.n = 0
#Specified a Slot for the run-function, enabling it to run on a seperate thread#
@Slot()                          
def run(self):
while self.go == True:
self.n += 1
#Enables us to freeze and start the plot via toggling self.pause (function bellow in this thread)#
while self.pause == True: 
time.sleep(0.01)
#This try-block generates data and updates the plots, over and over#
try:                       
data = np.sin(2*np.pi*(self.depths+0.001*self.n))
self.env_curve.setData(self.depths, data)
self.env_plot.setYRange(-1, 1)
#If an error occurs this block helps display it in the terminal#                  
except:                    
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
self.go = False
#This function is accessed if the 'start/stop'-button (specified in the main thread) is toggeled. After completion the while-loop above will halt or take off accordingly#            
def start_stop_worker(self):              
if self.pause == False:        
self.pause = True
elif self.pause == True:
self.pause = False

Here is the main thread where the GUI is built


###___Main-Thread___###
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
#Window name and dimensions#
self.setWindowTitle("My App")
self.setFixedWidth(800)
self.setFixedHeight(480)
#Plotting grid apperance#
pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k")
pg.setConfigOptions(antialias=True)
self.interval_range = [0,2*np.pi]
central_widget = QWidget()
plot_lay = QVBoxLayout(central_widget)
self.win = pg.GraphicsLayoutWidget()
plot_lay.addWidget(self.win)
btn_1 = QPushButton('Start')
btn_2 = QPushButton('Pause/Resume')
btn_lay = QHBoxLayout(central_widget)
plot_lay.addWidget(btn_1)
plot_lay.addWidget(btn_2)
plot_lay.addLayout(btn_lay)
self.env_plot = self.win.addPlot()
self.env_plot.showGrid(x=True, y=True)
self.env_plot.setLabel("bottom", "Depth (m)")
self.env_plot.setLabel("left", "Amplitude")
self.env_curve = self.env_plot.plot(pen=pg.mkPen("k", width=2))
#The connection for each button to one or several functions#
btn_1.pressed.connect(self.start_live_plot)
btn_2.pressed.connect(self.start_stop)
#Finally creating and setting a central widget that will contain everything we have created#
central_widget.setLayout(plot_lay)
self.setCentralWidget(central_widget)
#Goes to the live plot-tab and starts the Worker-thread#
def start_live_plot(self):
self.threadpool = QThreadPool()
self.worker = Worker(self.interval_range, self.env_curve, self.env_plot)
self.threadpool.start(self.worker)
#Accesses the start stop function in the worker thread#
def start_stop(self):
self.worker.start_stop_worker()
app = QApplication([])
window = MainWindow()
window.show()
app.exec()

Edit: I simplified the program and scaled down the text after feedback

答案1

得分: 0

我已解决了我的问题。显然,在单独的线程内更新图表会很麻烦。在更新后的版本中,我将以下代码移到了主线程的一个单独函数中:

self.env_curve.setData(self.depths, data)
self.env_plot.setYRange(-1, 1)

另外,创建了一个新的信号 result = Signal(object),用于在Worker线程和主线程之间传递每组新数据点的 self.data

英文:

I've resolved my own problem. Apparently it's troublesome to update the plot within a separate thread. In a updated version I've moved the lines

self.env_curve.setData(self.depths, data)
self.env_plot.setYRange(-1, 1)

to a separate function in the main thread. Also a new signal result = Signal(object) is created to emit the self.data from the Worker thread to the main thread for every new set of data points.

huangapple
  • 本文由 发表于 2023年7月20日 15:20:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76727524.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定