PySide6+pyqtgraph多线程绘图无报错冻结问题求助
问题:PySide6 + pyqtgraph 绘图长时间运行后冻结
目标
开发一款展示传感器数据的GUI,用正弦函数模拟数据,窗口包含「启动绘图」「暂停/恢复」两个操作按钮。
实现方式
- GUI基于PySide6构建,绘图组件采用pyqtgraph
- 数据生成与绘图逻辑放在独立Worker线程执行,主线程负责按钮交互和UI组件初始化
症状
图表会突然冻结,通常10-20分钟出现,操作GUI按钮会加速冻结;在树莓派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 @Slot() def run(self): while self.go == True: self.n += 1 while self.pause == True: time.sleep(0.01) 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) 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) def start_stop(self): self.worker.start_stop_worker() app = QApplication([]) window = MainWindow() window.show() app.exec()
问题根源与修复方案
核心问题
- 跨线程操作UI组件:Qt规定所有GUI组件必须在主线程(UI线程)中操作,你的Worker线程直接调用
self.env_curve.setData()和self.env_plot.setYRange(),属于非法跨线程操作,会引发线程安全问题,积累后导致UI冻结。 - 线程控制变量无保护:
self.pause等控制变量在多线程中读写,没有加锁,可能出现竞态条件,导致线程逻辑异常。
修复后代码
信号类与Worker线程代码
import sys import traceback import numpy as np import time from PySide6.QtCore import (Qt, QObject, QRunnable, QThreadPool, Slot, Signal, QMutex, QMutexLocker) from PySide6.QtWidgets import (QMainWindow, QApplication, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget) import pyqtgraph as pg ###___线程间通信信号___### class WorkerSignals(QObject): update_plot = Signal(np.ndarray, np.ndarray) # 传递x、y数据给主线程更新UI error = Signal(tuple) ###___Worker线程___### class Worker(QRunnable): def __init__(self, interval_range): super().__init__() self.signals = WorkerSignals() self.interval_range = interval_range # 绘图参数初始化 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._go = True self.n = 0 self.mutex = QMutex() @Slot() def run(self): while self._go: # 加锁读取控制变量 with QMutexLocker(self.mutex): if self._pause: time.sleep(0.01) continue self.n += 1 try: data = np.sin(2*np.pi*(self.depths+0.001*self.n)) # 发送信号,让主线程更新UI self.signals.update_plot.emit(self.depths, data) 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): with QMutexLocker(self.mutex): self._pause = not self._pause def stop_worker(self): with QMutexLocker(self.mutex): self._go = False
主线程GUI代码
###___主线程GUI___### class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("传感器数据展示") self.setFixedWidth(800) self.setFixedHeight(480) # pyqtgraph全局配置 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('启动绘图') btn_2 = QPushButton('暂停/恢复') plot_lay.addWidget(btn_1) plot_lay.addWidget(btn_2) # 绘图组件初始化 self.env_plot = self.win.addPlot() self.env_plot.showGrid(x=True, y=True) self.env_plot.setLabel("bottom", "深度 (m)") self.env_plot.setLabel("left", "振幅") self.env_plot.setYRange(-1, 1) # Y范围固定,仅初始化时设置一次 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) self.setCentralWidget(central_widget) # 线程池仅初始化一次 self.threadpool = QThreadPool() self.worker = None def start_live_plot(self): # 避免重复启动线程 if self.worker is None or not self.worker.isRunning(): self.worker = Worker(self.interval_range) # 连接线程信号到主线程UI更新函数 self.worker.signals.update_plot.connect(self.update_plot) self.worker.signals.error.connect(self.handle_error) self.threadpool.start(self.worker) def start_stop(self): if self.worker: self.worker.start_stop_worker() @Slot(np.ndarray, np.ndarray) def update_plot(self, x_data, y_data): # 主线程安全更新UI self.env_curve.setData(x_data, y_data) def handle_error(self, error_tuple): exctype, value, traceback_str = error_tuple print(f"线程错误: {exctype.__name__}: {value}") print(traceback_str) def closeEvent(self, event): # 关闭程序时停止线程 if self.worker: self.worker.stop_worker() event.accept() app = QApplication([]) window = MainWindow() window.show() app.exec()
额外优化建议
- 控制更新频率:在Worker线程的
run函数末尾添加time.sleep(0.05),将绘图更新频率控制在20Hz,减少主线程压力 - 树莓派环境优化:关闭抗锯齿
pg.setConfigOptions(antialias=False),降低GPU负载 - 按钮状态同步:可以根据线程暂停状态修改按钮文本,提升用户体验
内容的提问来源于stack exchange,提问作者Batmannilsson
相关产品推荐
相关产品推荐

