在线程操作期间避免PyQt5 GUI冻结?

时间:2017-11-02 17:57:28

标签: python multithreading pyqt pyqt5 python-multithreading

我在使用文件保存操作时冻结的GUI时遇到了一些麻烦,这需要一段时间,我很想知道为什么会这样。

我按照Schollii's wonderful answer上的similar question的说明操作,但必须有一些我不知道的东西,因为我无法按照我的预期运行GUI。

以下示例不可运行,因为它只显示相关部分,但希望它足以让讨论继续进行。基本上我有一个生成一些大数据的主应用程序类,我需要将它保存为HDF5格式,但这需要一些时间。为了使GUI响应,主类创建Saver类的对象和QThread以进行实际数据保存(使用moveToThread)。

此代码的输出几乎是我所期望的(即我看到一条消息“保存线程”具有与“主”线程不同的线程ID)所以我知道正在创建另一个线程。数据也已成功保存,因此该部件正常工作。

在实际数据保存期间(可能需要几分钟),GUI冻结并在Windows上“无响应”。关于出了什么问题的任何线索?

跑步期间的标准输出:

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 个答案:

答案 0 :(得分:1)

这里实际上有两个问题:(1)Qt的信号和插槽机制,以及(2)h5py

首先是信号/插槽。这些实际上通过传递给信号的复制参数来工作,以避免任何竞争条件。 (这只是你在Qt C ++代码中看到许多带有指针参数的信号的原因之一:复制指针很便宜。)因为你在主线程中生成数据,所以必须在主线程中复制它线程的事件循环。数据显然足够大,需要一些时间,阻止事件循环处理GUI事件。如果您(为了测试目的)在Saver.save_data()槽内生成数据,则GUI仍然保持响应。

但是,在打印第一条"Saving data from thread..."消息后,您现在会注意到一个小的延迟,表示在实际保存期间主事件循环被阻止。这是h5py的用武之地。

您可能会在文件顶部导入h5py,这是&#34;正确的&#34;要做的事。我注意到如果你在创建文件之前直接import h5py,那就消失了。我最好的猜测是涉及全局解释器锁,因为h5py代码可以从主线程和保存线程中看到。我原本期望主线程完全在Qt模块代码中,但是,GIL无法控制。所以,就像我说的那样,我不确定是什么原因造成阻塞。

就解决方案而言,如果你可以做我在这里描述的那样,那将解决问题。如果可能,建议在主线程外部生成数据。也可以将一些memoryview对象或numpy.view对象传递给保存线程,但是您必须自己处理线程同步。另外,在h5py广告位中导入Saver.save_data()会有所帮助,但如果您需要代码中的其他位置的模块,则不可行。

希望这有帮助!