在线程操作期间避免 PyQt5 GUI 冻结?
Avoiding PyQt5 GUI freeze during threaded operation?
我遇到了一些问题,GUI 在文件保存操作过程中冻结,这需要一些时间,我很想知道这是为什么。
我已按照 Schollii's wonderful answer on a 的说明进行操作,但我一定缺少某些东西,因为我无法让 GUI 像我期望的那样运行。
下面的示例无法运行,因为它只显示了相关部分,但希望它足以引发讨论。基本上我有一个生成一些大数据的主应用程序 class,我需要将其保存为 HDF5 格式,但这需要一些时间。为了让 GUI 保持响应,主要的 class 创建了一个 Saver
class 的对象和一个 QThread
的对象来进行实际的数据保存(使用 moveToThread
)。
这段代码的输出几乎是我所期望的(即我看到一条消息,表明 "saving thread" 的线程 ID 与 "main" 线程不同)所以我知道另一个线程正在创建。数据也已成功保存,因此该部分工作正常。
但是在实际数据保存期间(这可能需要几分钟),GUI 冻结并在 Windows 上变为 "Not responding"。有什么问题的线索吗?
运行 期间的标准输出:
outer thread "main" (#15108)
<__main__.Saver object at 0x0000027BEEFF3678> running SaveThread
Saving data from thread "saving_thread" (#13624)
代码示例:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot, QObject
class MyApp(QtWidgets.QMainWindow, MyAppDesign.Ui_MainWindow):
def save_file(self):
self.save_name, _ = QtWidgets.\
QFileDialog.getSaveFileName(self)
QThread.currentThread().setObjectName('main')
outer_thread_name = QThread.currentThread().objectName()
outer_thread_id = int(QThread.currentThreadId())
# print debug info about main app thread:
print('outer thread "{}" (#{})'.format(outer_thread_name,
outer_thread_id))
# Create worker and thread to save the data
self.saver = Saver(self.data,
self.save_name,
self.compressionSlider.value())
self.save_thread = QThread()
self.save_thread.setObjectName('saving_thread')
self.saver.moveToThread(self.save_thread)
# Connect signals
self.saver.sig_done.connect(self.on_saver_done)
self.saver.sig_msg.connect(print)
self.save_thread.started.connect(self.saver.save_data)
self.save_thread.start())
@pyqtSlot(str)
def on_saver_done(self, filename):
print('Finished saving {}'.format(filename))
''' End Class '''
class Saver(QObject):
sig_done = pyqtSignal(str) # worker id: emitted at end of work()
sig_msg = pyqtSignal(str) # message to be shown to user
def __init__(self, data_to_save, filename, compression_level):
super().__init__()
self.data = data_to_save
self.filename = filename
self.compression_level = compression_level
@pyqtSlot()
def save_data(self):
thread_name = QThread.currentThread().objectName()
thread_id = int(QThread.currentThreadId())
self.sig_msg.emit('Saving data '
'from thread "{}" (#{})'.format(thread_name,
thread_id))
print(self, "running SaveThread")
h5f = h5py.File(self.filename, 'w')
h5f.create_dataset('data',
data=self.data,
compression='gzip',
compression_opts=self.compression_level)
h5f.close()
self.sig_done.emit(self.filename)
''' End Class '''
这里其实有两个问题:(1)Qt的信号槽机制,(2)h5py
.
首先是signals/slots。这些实际上通过 复制 传递给信号的参数来工作,以避免任何竞争条件。 (这只是您在 Qt C++ 代码中看到如此多带有指针参数的信号的原因之一:复制指针很便宜。)因为您在主线程中生成数据,所以必须在主线程的事件中复制它环形。数据显然足够大,需要花费一些时间,从而阻止事件循环处理 GUI 事件。如果您改为(出于测试目的)在 Saver.save_data()
插槽内生成数据,GUI 将保持响应。
但是,您现在会注意到在打印第一个 "Saving data from thread..."
消息后 有一个小延迟 ,这表明在实际保存期间主事件循环被阻塞。这就是 h5py
的用武之地。
您可能在文件顶部导入 h5py
,这是 "correct" 要做的事情。我注意到,如果您直接在创建文件之前改用 import h5py
,这种情况就会消失。我最好的猜测是涉及全局解释器锁,因为 h5py
代码在主线程和保存线程中都是可见的。我本以为此时主线程将完全在 Qt 模块代码中,但是 GIL 无法控制它。所以,就像我说的,我不确定是什么导致了这里的阻塞。
至于解决方案,只要你能按照我在这里描述的去做,就会缓解问题。如果可能,建议在主线程之外生成数据。也可以将一些 memoryview
对象或 numpy.view
对象传递给保存线程,尽管您随后必须自己处理线程同步。此外,在 Saver.save_data()
插槽中导入 h5py
会有所帮助,但如果您在代码的其他地方需要该模块,则不可行。
希望对您有所帮助!
我遇到了一些问题,GUI 在文件保存操作过程中冻结,这需要一些时间,我很想知道这是为什么。
我已按照 Schollii's wonderful answer on a
下面的示例无法运行,因为它只显示了相关部分,但希望它足以引发讨论。基本上我有一个生成一些大数据的主应用程序 class,我需要将其保存为 HDF5 格式,但这需要一些时间。为了让 GUI 保持响应,主要的 class 创建了一个 Saver
class 的对象和一个 QThread
的对象来进行实际的数据保存(使用 moveToThread
)。
这段代码的输出几乎是我所期望的(即我看到一条消息,表明 "saving thread" 的线程 ID 与 "main" 线程不同)所以我知道另一个线程正在创建。数据也已成功保存,因此该部分工作正常。
但是在实际数据保存期间(这可能需要几分钟),GUI 冻结并在 Windows 上变为 "Not responding"。有什么问题的线索吗?
运行 期间的标准输出:
outer thread "main" (#15108)
<__main__.Saver object at 0x0000027BEEFF3678> running SaveThread
Saving data from thread "saving_thread" (#13624)
代码示例:
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot, QObject
class MyApp(QtWidgets.QMainWindow, MyAppDesign.Ui_MainWindow):
def save_file(self):
self.save_name, _ = QtWidgets.\
QFileDialog.getSaveFileName(self)
QThread.currentThread().setObjectName('main')
outer_thread_name = QThread.currentThread().objectName()
outer_thread_id = int(QThread.currentThreadId())
# print debug info about main app thread:
print('outer thread "{}" (#{})'.format(outer_thread_name,
outer_thread_id))
# Create worker and thread to save the data
self.saver = Saver(self.data,
self.save_name,
self.compressionSlider.value())
self.save_thread = QThread()
self.save_thread.setObjectName('saving_thread')
self.saver.moveToThread(self.save_thread)
# Connect signals
self.saver.sig_done.connect(self.on_saver_done)
self.saver.sig_msg.connect(print)
self.save_thread.started.connect(self.saver.save_data)
self.save_thread.start())
@pyqtSlot(str)
def on_saver_done(self, filename):
print('Finished saving {}'.format(filename))
''' End Class '''
class Saver(QObject):
sig_done = pyqtSignal(str) # worker id: emitted at end of work()
sig_msg = pyqtSignal(str) # message to be shown to user
def __init__(self, data_to_save, filename, compression_level):
super().__init__()
self.data = data_to_save
self.filename = filename
self.compression_level = compression_level
@pyqtSlot()
def save_data(self):
thread_name = QThread.currentThread().objectName()
thread_id = int(QThread.currentThreadId())
self.sig_msg.emit('Saving data '
'from thread "{}" (#{})'.format(thread_name,
thread_id))
print(self, "running SaveThread")
h5f = h5py.File(self.filename, 'w')
h5f.create_dataset('data',
data=self.data,
compression='gzip',
compression_opts=self.compression_level)
h5f.close()
self.sig_done.emit(self.filename)
''' End Class '''
这里其实有两个问题:(1)Qt的信号槽机制,(2)h5py
.
首先是signals/slots。这些实际上通过 复制 传递给信号的参数来工作,以避免任何竞争条件。 (这只是您在 Qt C++ 代码中看到如此多带有指针参数的信号的原因之一:复制指针很便宜。)因为您在主线程中生成数据,所以必须在主线程的事件中复制它环形。数据显然足够大,需要花费一些时间,从而阻止事件循环处理 GUI 事件。如果您改为(出于测试目的)在 Saver.save_data()
插槽内生成数据,GUI 将保持响应。
但是,您现在会注意到在打印第一个 "Saving data from thread..."
消息后 有一个小延迟 ,这表明在实际保存期间主事件循环被阻塞。这就是 h5py
的用武之地。
您可能在文件顶部导入 h5py
,这是 "correct" 要做的事情。我注意到,如果您直接在创建文件之前改用 import h5py
,这种情况就会消失。我最好的猜测是涉及全局解释器锁,因为 h5py
代码在主线程和保存线程中都是可见的。我本以为此时主线程将完全在 Qt 模块代码中,但是 GIL 无法控制它。所以,就像我说的,我不确定是什么导致了这里的阻塞。
至于解决方案,只要你能按照我在这里描述的去做,就会缓解问题。如果可能,建议在主线程之外生成数据。也可以将一些 memoryview
对象或 numpy.view
对象传递给保存线程,尽管您随后必须自己处理线程同步。此外,在 Saver.save_data()
插槽中导入 h5py
会有所帮助,但如果您在代码的其他地方需要该模块,则不可行。
希望对您有所帮助!