PyQt崩溃和线程安全

5

大家好,StackExchange社区的朋友们:

首先,非常感谢你们给了我很多帮助。这是我第一次提问:

我正在编写一个PyQt GUI应用程序,我发现它在Windows系统上崩溃,同时在我的家用电脑上出现了段错误,而在工作用的电脑(都是Linux Mint 17)上则可以运行。经过一些研究,我意识到我可能创建了一个线程不安全的GUI,因为我有几个对象调用彼此的方法。

来自另一个stackoverflow问题GUI小部件只能从主线程访问,也就是调用QApplication.exec()的线程。从任何其他线程访问GUI小部件 - 即您通过调用self.parent()进行的操作 - 是未定义的行为,在您的情况下会导致崩溃。

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

因此,我最终发现我应该只使用信号槽系统。

  1. 这是正确的吗?
  2. 这仅适用于函数调用,还是我可以以线程安全的方式从其他对象运行时操纵某些对象的字段?例如,我有一个选项对象,多个其他对象都可以访问它,并且我经常从不同的来源更改其中的参数。是线程安全还是不安全?

接下来,我有一个问题是,我无法在示例代码中重现这个线程不安全的行为。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. 为什么?这真的是线程不安全的,可能会崩溃,但它却没有崩溃吗?

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

3个回答

3

这是正确的吗?

是的,你应该只在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?

为什么这段代码不是线程安全的,但却没有崩溃呢?

线程不安全并不意味着“崩溃是必然的”,而是指“可能会崩溃”或“有很高的崩溃概率”。

来自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的某些变化。或者没有? - grg rsr
@grgrsr 这只是一个举例说明,当您发出信号时,您可以仅发出已更改的选项值,而不是整个选项对象,这样您就不会执行跨线程函数调用。是的,在设置选项后,会发出信号通知所有连接到它的对象某些选项已更改。 - Raydel Miranda
1
@RaydelMiranda。您的回答大部分是正确的,但您声称"每个控件都存在于自己的线程中"是完全错误的。您的示例代码只是显示每次调用self.thread()时创建了一个新的 QThread 实例。请记住,QThread 实际上并不是一个线程!它只是一个管理由操作系统提供的线程的包装器。 - ekhumoro
@ekhumoro,你是对的,我在API文档中进行了更深入的挖掘并在运行时调试了代码,没有每个QObject的线程这样的东西,谢谢,回答已编辑。 - Raydel Miranda

2

你的问题的答案:

  1. GUI小部件只能从主线程(运行QApplication.exec_()的线程)中访问。自Qt 4以来,信号和槽默认是线程安全的。

  2. 任何导致从主线程以外的另一个线程直接操作Qt图形对象的调用并非线程安全 => 将会崩溃。

  3. 在你的问题代码中没有涉及到线程(线程在哪里???),不是不同的QObjects位于不同的线程中这是错误的。也许你遇到的崩溃与线程无关?


感谢您的回答。如果self.thread()返回对象的线程,则它们确实存在于不同的线程中:在我的计算机上运行示例会产生以下结果: <PyQt4.QtCore.QThread object at 0x7f22a6099c30> <PyQt4.QtCore.QThread object at 0x7f22a6099d60>Qt文档 一个QObject实例被认为是存在于创建它的线程中。该对象的事件由该线程的事件循环分派。 QObject所在的线程可以使用QObject::thread()获得。还是我漏掉了什么? - grg rsr
@mguijarr 看一下我的回答(在最后),QObjects确实存在于不同的线程中。 - Raydel Miranda
我同意似乎这些QObjects存在于不同的线程中 - 但是,我非常惊讶!!!想象一下,如果您的应用程序中有100个小部件,那么Qt会创建101个线程吗?这太疯狂了。所以我在这里缺少一些东西...您有更多信息吗? - mguijarr
1
@mguijarr。你怀疑是正确的:请看我对Raydel答案的评论。 - ekhumoro
1
@mguijarr,抱歉,我错了!!!ekhumoro是正确的。我误解了API文档,在进行了几次实验后,我看到了真相。 - Raydel Miranda

1
作为对一些评论的回应,以下是一个测试脚本,展示了如何检查代码正在执行的线程:
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!那么...为了解决我的困惑,我的所有对象最终都生存在同一个线程中? - grg rsr
@grgrsr。至于你的错误程序,没有看到实际代码是无法诊断的,但最有可能的原因是对象所有权问题。崩溃只发生在程序退出时吗?如果是这样,那么这是一个非常普遍的问题,是由于Python在解释器关闭时以不可预测的顺序删除对象导致的。有各种各样的方法来修复它,但这很大程度上取决于应用程序编写的具体情况。 - ekhumoro
感谢对线程问题的澄清。至于崩溃问题(当时与之无关),它们不会在退出时发生,程序在我的Linux上运行良好,但在Windows上却不行。代码是一个更有结构的重写,之前已经存在并运行。现在我在信号/槽系统中实现了所有PyQt对象之间的通信,但崩溃仍然存在。有任何想法从哪里开始查找吗? - grg rsr
忘记添加了:崩溃发生在主窗口的 .show() 调用上。 - grg rsr
@grgrsr。正如我之前所说的那样:没有看到实际代码是不可能确定问题的。你需要尝试隔离问题,以便能够产生一个小的测试用例来重现这个问题。只需删除代码块,直到问题消失;然后开始加入较小的代码块,直到问题再次出现,如此往复。 - ekhumoro
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接