在线程操作期间避免 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 会有所帮助,但如果您在代码的其他地方需要该模块,则不可行。

希望对您有所帮助!