【问题标题】:Avoiding PyQt5 GUI freeze during threaded operation?在线程操作期间避免 PyQt5 GUI 冻结?
【发布时间】:2018-04-15 08:37:25
【问题描述】:

我在文件保存操作期间遇到了一些问题,GUI 在需要一些时间的过程中冻结,我很想了解这是为什么。

我已经按照similar question 上的Schollii's wonderful answer 的说明进行操作,但是我一定缺少一些东西,因为我无法让 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 '''

【问题讨论】:

    标签: python multithreading pyqt pyqt5 python-multithreading


    【解决方案1】:

    这里其实有两个问题:(1)Qt的信号槽机制,(2)h5py

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

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

    您大概是在文件顶部导入h5py,这是“正确”的做法。我注意到,如果您在创建文件之前直接使用import h5py,这就会消失。我最好的猜测是涉及到全局解释器锁,因为h5py 代码在主线程和保存线程中都是可见的。我原以为此时主线程将完全位于 Qt 模块代码中,但是 GIL 无法控制。所以,就像我说的,我不确定是什么导致了这里的阻塞。

    就解决方案而言,只要您可以按照我在此处描述的方式进行操作,就会缓解问题。如果可能,建议在主线程之外生成数据。也可以将一些memoryview 对象或numpy.view 对象传递给保存线程,但您必须自己处理线程同步。此外,在 Saver.save_data() 插槽中导入 h5py 会有所帮助,但如果您在代码的其他地方需要该模块,则不可行。

    希望这会有所帮助!

    【讨论】:

    • 感谢您的回复。我没有考虑复制数据所需的时间。你是对的,如果我在Saver.save_data() 中加载一些测试数据然后保存它,一切都会保持响应。问题是我有另一个生成数据的类,而我的主应用程序拥有该类的一个实例,因此以这种方式将数据传递给Saver。我不知道如何解决这个问题,但我会像你建议的那样使用memoryview 和 Numpy 视图
    • 在更多的测试中,我不认为是内存中的复制数据导致了问题。如果是这种情况,在初始化Saver 对象(即构造函数中的self.data = data_to_save 行)时不会发生锁定吗?似乎锁定发生在实际保存到磁盘期间(使用h5py 东西)
    • @jat255 是的,你是对的,我不知道为什么我认为数据是作为信号参数发出的。我想我改变了一些东西来做一个工作的例子。无论如何,关于在本地导入 h5py 的第二点似乎仍然是相关的。
    • 有趣的是,如果我直接保存为 numpy 对象(.npy 或 .npz),我不会得到任何类型的锁定,所以这是特定于 h5py 中发生的事情,看起来
    • @jat255 是的,我在h5py 代码中进行挖掘,如果它注意到多个线程正在访问同一个文件,它似乎会进行一些锁定。据我所知,它没有记录,但h5py._objects 共享库有一个FastRLock 对象,在这种情况下似乎实现了锁定。
    猜你喜欢
    • 1970-01-01
    • 2020-03-02
    • 2018-05-03
    • 2012-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-08
    相关资源
    最近更新 更多