PyQt 崩溃和线程安全

Posted

技术标签:

【中文标题】PyQt 崩溃和线程安全【英文标题】:PyQt crashes and thread safety 【发布时间】:2015-04-14 12:10:49 【问题描述】:

您好 StackExchange 社区,

首先,你们对我帮助很大,非常感谢。第一次提问:

我目前正在编写一个 PyQt GUI 应用程序,我发现它在 Windows 系统上崩溃,而且它在我家的机器上给我一个段错误,而它在工作的机器上工作(都是 linux mint 17)。经过一番研究,我意识到我可能创建了一个线程不安全的 GUI,因为我有几个对象相互调用方法。

From another *** question: GUI 小部件只能从主线程访问,即调用 QApplication.exec() 的线程。从任何其他线程访问 GUI 小部件——你对 self.parent() 的调用——是未定义的行为,在你的情况下这意味着崩溃。

From Qt docs: 虽然 QObject 是可重入的,但 GUI 类,尤其是 QWidget 及其所有子类,是不可重入的。它们只能在主线程中使用。如前所述,还必须从该线程调用 QCoreApplication::exec()。

所以最后,我认为我应该只使用信号槽系统来这样做。

    这是正确的吗? 这只是函数调用需要,还是我可以在运行时以线程安全的方式操作来自其他对象的某些对象的字段?例如,我有一个可以从多个其他对象访问的选项对象,并且我经常从不同的来源更改参数。线程安全还是不安全?

接下来,我在示例代码中重新创建这种线程不安全行为时遇到了问题。 Qt 文档说 QObjects 存在于不同的线程中。这意味着,以下 Qt 应用程序应该是线程不安全的(如果我理解正确的话)。

from PyQt4 import QtGui
import sys

class TestWidget(QtGui.QWidget):
    def __init__(self,string):
        super(TestWidget,self).__init__()
        self.button = QtGui.QPushButton(string,parent=self)
        self.button.clicked.connect(self.buttonClicked)
        
        # just to check, and yes, lives in it's own thread
        print self.thread()
        
    def buttonClicked(self):
        # the seemingly problematic line
        self.parent().parent().statusBar().showMessage(self.button.text())
        pass
    pass

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MainWindow,self).__init__()
        
        Layout = QtGui.QHBoxLayout()
        for string in ['foo','bar']:
            Layout.addWidget(TestWidget(string))
        
        CentralWidget = QtGui.QWidget(self)
        CentralWidget.setLayout(Layout)
        self.setCentralWidget(CentralWidget)
        self.statusBar()
        self.show()
        pass
    pass

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    M = MainWindow()
    sys.exit(app.exec_())

但它可以在我的机器上运行,也可以在 Windows 机器上正常运行。

    为什么?这实际上是线程不安全的并且可能会崩溃,但它不会吗?

感谢您帮我解决这个问题...

【问题讨论】:

【参考方案1】:

这是正确的吗?

是的,您应该只将信号槽系统用于 q 对象之间的交互。 这就是它的本意。

这只是函数调用需要,还是我可以操作一些对象的字段 在运行时以线程安全的方式从其他对象获取数据?

我有一个可以从多个其他对象访问的选项对象...

如果这里的对象是指 Q 对象:

你的options对象应该支持信号槽机制,你可以实现这个 从QObject 派生options

class Options(QtCore.QObject):
    optionUpdated = QtCore.pyqtSignal(object)

    def __init__(self):

        self.__options = 
            'option_1': None
        

    def get_option(self, option):
        return self.__options.get(option)

    def set_option(self, option, value):
        self.__options[option] = value
        self.optionUpdated.emit(self)

然后所有使用此选项的小部件/对象都应该有一个连接到此信号的插槽。

一个简单的例子:

    options = Options()
    some_widget = SomeWidget()
    options.optionUpdated.connect(some_widget.options_updated)    // Is like you implement the observer pattern, right?

为什么?这实际上是线程不安全的并且可能会崩溃,但事实并非如此?

thread-unsafe 并不是说​​“一定会崩溃”,而是“这可能会崩溃”或“这很有可能会崩溃”。

来自 pyqt API 文档QObject.thread:

返回对象所在的线程。

勘误

正如 ekumoro 所指出的,我已经重新检查了我之前关于每个对象离开不同线程的立场,并且......我错了!

QObject.thread 将为每个对象返回一个不同的 QThread 实例,但 QThread 实际上并不是一个线程,它只是操作系统提供的那些线程的包装器。

因此,在不同线程中分割多个对象时,代码实际上并没有问题。

为简单起见,我稍微修改了您用于演示的代码:

from PyQt4 import QtGui
import sys

class TestWidget(QtGui.QWidget):
    def __init__(self,string):
        super(TestWidget,self).__init__()
        # just to check, and yes, lives in it's own thread
        print("TestWidget thread: ".format(self.thread()))

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MainWindow,self).__init__()
        print("Window thread: ".format(self.thread()))
        Layout = QtGui.QHBoxLayout()
        for string in ['foo','bar']:
            Layout.addWidget(TestWidget(string))
        self.show()

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    M = MainWindow()
    sys.exit(app.exec_())

是的,打印出来的是:

Window thread: <PyQt4.QtCore.QThread object at 0x00000000025C1048>
TestWidget thread: <PyQt4.QtCore.QThread object at 0x00000000025C4168>
TestWidget thread: <PyQt4.QtCore.QThread object at 0x00000000025C41F8>

演示每个控件都存在于自己的线程中。

现在,您有了信号槽机制来处理这种“线程安全”,任何其他方法都不是线程安全的。

【讨论】:

感谢您的回答。但是我不理解示例代码,这一定是因为我在信号槽系统或线程中遗漏了一些东西。在您的代码中,外部对象通过调用 get_options 函数从 options 检索值。这样,如果我的外部对象存在于另一个线程中,它就会执行一个据说有问题的跨线程函数调用。现在在您的代码中,在设置值后发出信号,只是通知some_widget options 发生了什么事。还是不行? @grgrsr 这只是一个说明性示例,当您发出信号时,您可以仅发出已更改的选项值,而不是整个选项对象,这样您就不会执行跨线程函数调用.是的,在设置选项后,会发出一个信号,通知连接到它的所有对象某个选项已更改。 @RaydelMiranda。您的回答大多没问题,但是您声称“每个控件都存在于自己的线程中”的说法是完全错误的。您的所有示例代码显示的是,每次调用 self.thread() 时都会创建一个新的 QThread 实例。请记住,QThread 实际上并不是一个线程!它只是一个包装器,用于管理最终由操作系统提供的线程。 @ekhumoro,你说得对,我对 API 坞站做了更多的挖掘,并在运行时调试了代码,并且每个 QObject 都没有线程之类的东西,谢谢我编辑了答案。【参考方案2】:

回答您的问题:

    GUI 小部件只能从主线程(运行 QApplication.exec_())。信号和槽默认是线程安全的,因为 Qt 4

    任何导致 direct Qt 图形对象从另一个线程而不是主线程操作的调用 不是 线程安全 => 将崩溃

    您的问题代码中没有涉及线程(线程在哪里???), 不同的 QObject 存在于不同的线程中是不正确的。也许你所经历的崩溃无关 带螺纹?

【讨论】:

也感谢您的回答。如果self.thread() 返回对象的线程,那么是的,它们确实存在于不同的线程中:在我的计算机上运行示例会产生&lt;PyQt4.QtCore.QThread object at 0x7f22a6099c30&gt; &lt;PyQt4.QtCore.QThread object at 0x7f22a6099d60&gt; Qt docs 据说QObject 实例存在于它所在的线程中创建的。该对象的事件由该线程的事件循环分派。可以使用 QObject::thread() 获得 QObject 所在的线程。 或者我在这里缺少什么? @mguijarr 看看我的回答(最后),QObjects 确实存在于不同的线程中。 我同意这些 QObjects 似乎生活在不同的线程中 - 但是,我非常惊讶!!!想象一下,你的应用程序中有 100 个小部件,然后 Qt 创建了 101 个线程???这很疯狂。所以我在这里遗漏了一些东西......你有更多信息吗? @mguijarr。您对此表示怀疑是正确的:请参阅我对 Raydel 回答的评论。 @mguijarr,对不起,我错了!!!埃库莫罗是对的。我误解了 API 文档,做了几次实验后我明白了。【参考方案3】:

作为一些 cmets 的后续,下面是一个测试脚本,展示了如何检查代码在哪个线程中执行:

from PyQt4 import QtCore, QtGui

class Worker(QtCore.QObject):
    threadInfo = QtCore.pyqtSignal(object, object)

    @QtCore.pyqtSlot()
    def emitInfo(self):
        self.threadInfo.emit(self.objectName(), QtCore.QThread.currentThreadId())

class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.button = QtGui.QPushButton('Test', self)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.button)
        self.thread = QtCore.QThread(self)
        self.worker1 = Worker()
        self.worker1.setObjectName('Worker1')
        self.worker1.moveToThread(self.thread)
        self.worker1.threadInfo.connect(self.handleShowThreads)
        self.button.clicked.connect(self.worker1.emitInfo)
        self.worker2 = Worker()
        self.worker2.setObjectName('Worker2')
        self.worker2.threadInfo.connect(self.handleShowThreads)
        self.button.clicked.connect(self.worker2.emitInfo)
        self.thread.start()

    def handleShowThreads(self, name, identifier):
        print('Main: %s' % QtCore.QThread.currentThreadId())
        print('%s: %s\n' % (name, identifier))

    def closeEvent(self, event):
        self.thread.quit()
        self.thread.wait()

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

【讨论】:

谢谢,这很有帮助。不幸的是,我不能投票赞成你的答案,因为我没有足够的声誉......但作为后续问题:我已将 QtCore.QThread.currentThreadId() 放入每个对象的构造函数中,始终返回相同的 ID!所以......为了完成我的困惑,我所有的对象最终都生活在同一个线程中? @grgrsr。您的所有对象都将存在于创建它们的同一线程中,除非您使用moveToThread 将它们显式移动到另一个线程中。在您的示例代码中,只有一个线程(即主 GUI 线程),因此 currentThreadId 将始终返回相同的内容。在您的问题中,您说 Qt 文档声明“QObjects 存在于不同的线程中”。这不是真的,所以我想你一定是看错了什么。 @grgrsr。至于您的段错误程序:如果不查看实际代码就无法诊断,但这很可能是由对象所有权问题引起的。崩溃仅在程序退出时发生吗?如果是这样,这是一个很常见的问题,并且是由于解释器关闭时 python 以不可预知的顺序删除对象引起的。有多种方法可以修复它,但这在很大程度上取决于应用程序的具体编写方式。 感谢您对线程问题的澄清。对于崩溃的(当时不相关的)问题:它们不会在退出时发生,程序至少在我的 linux 上运行良好,但在 Windows 上却不行。代码是对以前存在并运行的程序的更结构化的重写。现在我在信号/插槽系统中实现了所有通信 btw PyQt 对象,但崩溃仍然存在。知道从哪里开始寻找吗? 忘了补充:在主窗口的 .show() 调用中发生崩溃

以上是关于PyQt 崩溃和线程安全的主要内容,如果未能解决你的问题,请参考以下文章

PyQT5 多线程问题

线程安全的概念

线程:PyQt 因“出队时队列中的未知请求”而崩溃

IOS:FMDB使用databaseQueue实现数据库操作线程安全

pyqt:如何正确退出线程

WPF 支持的多线程 UI 并不是线程安全的