为什么我的PyQt5和Vispy应用在QThread循环中加了sleep后才更新GUI?
基本上,我正在尝试用Vispy库实现一个PyQt5应用程序,用于实时数据可视化。在下面的代码中,我想绘制一个每秒10000个样本的正弦波,持续100秒。我把数据的创建和更新放在了一个QThread线程里,而图形的界面更新则在主线程中进行。然而,运行这段代码时,界面会卡住,无法在指定的时间间隔内更新。
from PyQt5.QtGui import QCloseEvent
from vispy.app import use_app, Timer
import numpy as np
from PyQt5 import QtWidgets, QtCore
from vispy import scene, visuals
import time
from scipy import signal
from collections import deque
import cProfile
import pstats
class MainWindow(QtWidgets.QMainWindow):
closing = QtCore.pyqtSignal()
def __init__(self, canvas_wrapper, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
central_widget = QtWidgets.QWidget()
main_layout = QtWidgets.QHBoxLayout()
self.canvas_wrapper = canvas_wrapper
main_layout.addWidget(self.canvas_wrapper.canvas.native)
central_widget.setLayout(main_layout)
self.setCentralWidget(central_widget)
def closeEvent(self, event):
print("Closing main window")
self.closing.emit()
return super().closeEvent(event)
class CanvasWrapper:
def __init__(self, update_interval = .016):
self.canvas = scene.SceneCanvas(keys='interactive', size=(600, 600), show=True)
self.grid = self.canvas.central_widget.add_grid()
title = scene.Label("Test Plot", color='white')
title.height_max = 40
self.grid.add_widget(title, row=0, col=0, col_span=2)
self.yaxis = scene.AxisWidget(orientation='left',
axis_label='Y Axis',
axis_font_size=12,
axis_label_margin=50,
tick_label_margin=10)
self.yaxis.width_max = 80
self.grid.add_widget(self.yaxis, row=1, col=0)
self.xaxis = scene.AxisWidget(orientation='bottom',
axis_label='X Axis',
axis_font_size=12,
axis_label_margin=50,
tick_label_margin=20)
self.xaxis.height_max = 80
self.grid.add_widget(self.xaxis, row=2, col=1)
right_padding = self.grid.add_widget(row=1, col=2, row_span=1)
right_padding.width_max = 50
self.view = self.grid.add_view(row=1, col=1, border_color='white')
self.view.camera = "panzoom"
self.data = np.empty((2, 2))
self.line = scene.Line(self.data, parent=self.view.scene)
self.xaxis.link_view(self.view)
self.yaxis.link_view(self.view)
self.update_interval = update_interval
self.last_update_time = time.time()
def update_data(self, newData):
if self.should_update():
data_array = newData["data"]
data_array = np.array(data_array)
x_min, x_max = data_array[:, 0].min(), data_array[:, 0].max()
y_min, y_max = data_array[:, 1].min(), data_array[:, 1].max()
self.view.camera.set_range(x=(x_min, x_max), y=(y_min, y_max))
self.line.set_data(data_array)
def should_update(self):
current_time = time.time()
if current_time - self.last_update_time >= self.update_interval:
self.last_update_time = current_time
return True
return False
class DataSource(QtCore.QObject):
new_data = QtCore.pyqtSignal(dict)
finished = QtCore.pyqtSignal()
def __init__(self, sample_rate, seconds, seconds_to_display=15, q = 100, parent = None):
super().__init__(parent)
self.count = 0
self.q = q
self.should_end = False
self.sample_rate = sample_rate
self.num_samples = seconds*sample_rate
self.seconds_to_display = seconds_to_display
size = self.seconds_to_display*self.sample_rate
self.buffer = deque(maxlen=size)
def run_data_creation(self):
print("Run Data Creation is starting")
for count in range (self.num_samples):
if self.should_end:
print("Data saw it was told to end")
break
self.update(self.count)
self.count += 1
data_dict = {
"data": self.buffer,
}
self.new_data.emit(data_dict)
print("Data source finished")
self.finished.emit()
def stop_data(self):
print("Data source is quitting...")
self.should_end = True
def update(self, count):
x_value = count / self.sample_rate
y_value = np.sin((count / self.sample_rate) * np.pi)
self.buffer.append([x_value, y_value])
class Main:
def __init__(self, sample_rate, seconds, seconds_to_display):
# Set up the application to use PyQt5
self.app = use_app("pyqt5")
self.app.create()
# Set some parameters for the data source
self.sample_rate, self.seconds, self.seconds_to_display = sample_rate, seconds, seconds_to_display
# Create the canvas wrapper and main window
self.canvas_wrapper = CanvasWrapper()
self.win = MainWindow(self.canvas_wrapper)
# Set up threading for the data source
self.data_thread = QtCore.QThread(parent=self.win)
self.data_source = DataSource(self.sample_rate, self.seconds)
# Move the data source to the thread
self.data_source.moveToThread(self.data_thread)
# Connect signals and slots for data updates and thread management
self.setup_connections()
def setup_connections(self):
self.data_source.new_data.connect(self.canvas_wrapper.update_data)
self.data_thread.started.connect(self.data_source.run_data_creation)
self.data_source.finished.connect(self.data_thread.quit, QtCore.Qt.DirectConnection)
self.win.closing.connect(self.data_source.stop_data, QtCore.Qt.DirectConnection)
self.data_thread.finished.connect(self.data_source.deleteLater)
def run(self):
self.win.show()
self.data_thread.start()
self.app.run()
self.data_thread.quit()
self.data_thread.wait(5000)
def profile_run():
visualization_app = Main(10000, 100, 10)
visualization_app.run()
if __name__ == "__main__":
cProfile.run('profile_run()', 'profile_out')
stats = pstats.Stats('profile_out')
stats.sort_stats('cumulative')
stats.print_stats(10)
stats.sort_stats('time').print_stats(10)
只有当我在数据创建的函数里加上一个暂停时,界面才会更新(不过即使加了小的暂停,更新速度也很慢)。为什么QThread会这样?我该怎么解决这个问题呢?
def run_data_creation(self):
print("Run Data Creation is starting")
for count in range (self.num_samples):
if self.should_end:
print("Data saw it was told to end")
break
self.update(self.count)
self.count += 1
time.sleep(.0000001)
data_dict = {
"data": self.buffer,
}
self.new_data.emit(data_dict)
print("Data source finished")
self.finished.emit()
1 个回答
回答 2
如果你把 update_data
里的两个 vispy 相关的代码注释掉,并且可以选择性地加一些打印语句,你会发现界面还是一样的,依然会卡住。所以这跟 vispy 没关系。
对于实时应用来说,一般的规则是,任何不涉及可视化的操作都应该放在单独的线程里(也就是数据源)。这包括你的更新间隔逻辑。换句话说,只有在需要更新可视化的时候才发送数据。
从 Qt 的事件循环来看,你基本上是在把成千上万的事件放到 Qt 需要处理的任务队列里。在 Qt 处理完你的数据事件之前,它无法处理任何新的用户界面事件。这就像是在 Python 中对成千上万的事件进行一个 for
循环。
解决方案,至少在开始时,是把所有的更新间隔逻辑放在数据源线程里。只有当数据准备好要显示的时候才发出信号。而且,如果你的目标是每秒处理 10000 个点,这可是一大堆数据,要实时查看的话可能会很吃力。你可能需要考虑对数据进行平均处理或者选择性地减少更新数据的频率。
回答 1
看起来这个是基于 vispy 的实时数据示例,对吧?我写这些代码的时候不太记得我在没有休眠的数据源上测试了多少,所以这段代码可能并没有按照我预期的那样运行。
你提到这不是在“指定的间隔”内更新,这是什么意思呢?可能是你给界面发送了太多的更新事件,以至于它无法在你看到最终结果之前完成更新。如果没有你的休眠(如果我说错了请纠正我),你基本上是以 CPU 能够达到的最快速度从第一轮迭代跑到最后一轮,对吧?在这种情况下,你期待看到什么呢?在一个更现实的例子中,生成数据的数据源会花费一些实际的时间,但你的例子是瞬间生成所有数据。你确定应用程序是卡住了,还是只是完成了所有数据的生成?
如果我上面说的都错了,那么我看到的一个不同之处是你使用了 deque
,而我对这个的经验很少。我在想,如果你在数据源里每次迭代都创建一个新的 numpy 数组,行为会不会有什么不同。