在线程间发射None时PySide使Python崩溃

6 投票
1 回答
1191 浏览
提问于 2025-04-18 06:57

[编辑] 这个问题并不是和PySide发出信号导致Python崩溃的问题完全相同。这个问题特别涉及到一个(现在已经)PySide中的已知bug,导致无法在不同线程之间传递None。另一个问题是关于如何将信号连接到旋转框。我已经更新了这个问题的标题,以更好地反映我所面临的问题。[/编辑]

我在使用PySide时遇到了一个情况,它的表现和PyQt有些微妙的不同。其实我说的“微妙”是因为PySide会让Python崩溃,而PyQt则按我预期的那样工作。

在PySide下我的应用崩溃

我对PySide完全是新手,对PyQt也还不算太熟悉,所以可能我犯了一些基本错误,但我真的是搞不懂……真的希望你们能给我一些建议!

整个应用是一个批处理工具,描述起来太复杂了,但我把问题简化到了下面的代码示例中:

import threading

try:
    # raise ImportError()  # Uncomment this line to show PyQt works correctly
    from PySide import QtCore, QtGui
except ImportError:
    from PyQt4 import QtCore, QtGui
    QtCore.Signal = QtCore.pyqtSignal
    QtCore.Slot = QtCore.pyqtSlot


class _ThreadsafeCallbackHelper(QtCore.QObject):
    finished = QtCore.Signal(object)


def Dummy():
    print "Ran Dummy"
    # return ''  # Uncomment this to show PySide *not* crashing
    return None


class BatchProcessingWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self, None)

        btn = QtGui.QPushButton('do it', self)
        btn.clicked.connect(lambda: self._BatchProcess())

    def _BatchProcess(self):
        def postbatch():
            pass
        helper = _ThreadsafeCallbackHelper()
        helper.finished.connect(postbatch)

        def cb():
            res = Dummy()
            helper.finished.emit(res)  # `None` crashes Python under PySide??!
        t = threading.Thread(target=cb)
        t.start()


if __name__ == '__main__':  # pragma: no cover
    app = QtGui.QApplication([])
    BatchProcessingWindow().show()
    app.exec_()

运行这个代码会显示一个带有“执行”按钮的窗口。如果在PySide下点击这个按钮,Python就会崩溃。如果取消注释第4行的ImportError,你会看到PyQt*可以正确运行Dummy函数。或者取消注释第20行的return语句,你会看到PySide可以正确运行。

我不明白为什么发出None会让Python/PySide崩溃得这么严重?

我的目标是将处理(无论Dummy做什么)转移到另一个线程,以保持主GUI线程的响应性。再次强调,这在PyQt下工作得很好,但在PySide下显然不行。

任何建议都将非常感谢。

这是在:

    Python 2.7 (r27:82525, Jul  4 2010, 09:01:59) [MSC v.1500 32 bit (Intel)] on win32

    >>> import PySide
    >>> PySide.__version_info__
    (1, 1, 0, 'final', 1)

    >>> from PyQt4 import Qt
    >>> Qt.qVersion()
    '4.8.2'

1 个回答

2

所以,如果有人说PySide被忽视了,而这确实是个bug,那我们不如想个办法来解决它,对吧?

通过引入一个特殊的标记来替代None,然后发出,就可以绕过这个问题。接着在回调函数中再把这个特殊标记换回None,这样问题就解决了。

真是让人感到无奈。不过,我会把我最终写的代码发出来,欢迎大家提出意见。如果你有更好的办法或者真正的解决方案,请一定告诉我。与此同时,我想这个办法也能凑合用:

_PYSIDE_NONE_SENTINEL = object()


def pyside_none_wrap(var):
    """None -> sentinel. Wrap this around out-of-thread emitting."""
    if var is None:
        return _PYSIDE_NONE_SENTINEL
    return var


def pyside_none_deco(func):
    """sentinel -> None. Decorate callbacks that react to out-of-thread
    signal emitting.

    Modifies the function such that any sentinels passed in
    are transformed into None.
    """

    def sentinel_guard(arg):
        if arg is _PYSIDE_NONE_SENTINEL:
            return None
        return arg

    def inner(*args, **kwargs):
        newargs = map(sentinel_guard, args)
        newkwargs = {k: sentinel_guard(v) for k, v in kwargs.iteritems()}
        return func(*newargs, **newkwargs)

    return inner

修改我原来的代码后,我们得到了这个解决方案:

class _ThreadsafeCallbackHelper(QtCore.QObject):
    finished = QtCore.Signal(object)


def Dummy():
    print "Ran Dummy"
    return None


def _BatchProcess():
    @pyside_none_deco
    def postbatch(result):
        print "Post batch result: %s" % result

    helper = _ThreadsafeCallbackHelper()
    helper.finished.connect(postbatch)

    def cb():
        res = Dummy()
        helper.finished.emit(pyside_none_wrap(res))

    t = threading.Thread(target=cb)
    t.start()


class BatchProcessingWindow(QtGui.QDialog):
    def __init__(self):
        super(BatchProcessingWindow, self).__init__(None)

        btn = QtGui.QPushButton('do it', self)
        btn.clicked.connect(_BatchProcess)


if __name__ == '__main__':  # pragma: no cover
    app = QtGui.QApplication([])
    window = BatchProcessingWindow()
    window.show()
    sys.exit(app.exec_())

我怀疑这个解决方案不会有什么大奖,但看起来确实解决了问题。

撰写回答