How to send None with Signals across threads?

2019-07-09 03:18发布

问题:

I've implemented a version of the worker pattern that is described in the Qt Threading docs.

I'm using Signals/Slots to send data between the worker thread and the main thread.

When defining the Signal, I've set the argument signature type to object since I believe it should allow me to pass any python object through the Signal.

result_ready = QtCore.Signal(object)

However, when I try to pass None through the Signal it crashes python. This only happens when trying to pass the Signal across threads. If I comment out the self.worker.moveToThread(self.thread) line, it works and None is successfully passed through the Signal.

Why am I unable to pass None in this instance?

I'm using PySide 1.2.2 and Qt 4.8.5.

import sys

from PySide import QtCore, QtGui


class Worker(QtCore.QObject):

    result_ready = QtCore.Signal(object)

    @QtCore.Slot()
    def work(self):
        print 'In Worker'

        # This works
        self.result_ready.emit('Value')

        # This causes python to crash
        self.result_ready.emit(None)


class Main(QtGui.QWidget):

    def __init__(self):
        super(Main, self).__init__()
        self.ui_lay = QtGui.QVBoxLayout()
        self.setLayout(self.ui_lay)
        self.ui_btn = QtGui.QPushButton('Test', self)
        self.ui_lay.addWidget(self.ui_btn)
        self.ui_lay.addStretch()
        self.setGeometry(400, 400, 400, 400)
        self.worker = Worker()
        self.thread = QtCore.QThread(self)
        self.worker.moveToThread(self.thread)
        self.thread.start()
        self.ui_btn.clicked.connect(self.worker.work)
        self.worker.result_ready.connect(self.handle_worker_result)

    @QtCore.Slot(object)
    def handle_worker_result(self, result=None):
        print 'Handling output', result

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


if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    obj = Main()
    obj.show()
    app.exec_()

回答1:

This looks like a PySide bug. The same example code works exactly as expected with PyQt4.

The issue is with the type of signal connection. For cross-thread signals, this will use a QueuedConnection unless you specify otherwise. If the connection type is changed to DirectConnection in the example code, it will work as expected - but of course it won't be thread-safe anymore.

A QueuedConnection will post an event to the event-queue of the receiving thread. But in order for this to be thread-safe, Qt has to serialize the emitted arguments. However, PySide will obviously need to inject some magic here to deal with python types that Qt doesn't know anything about. If I had to guess, I would bet that PySide is mistakenly converting the python None object to a C++ NULL pointer, which will obviously have nasty consequences later on.

If you want to work around this, I suppose you could emit your own sentinel object as a placeholder for None.

UPDATE:

Found the bug, PYSIDE-17, which was posted in March 2012! Sadly, the suggested patch seems to have never been reviewed.



回答2:

If you change you signal to a proper class then it works fine for me. eg :

result_ready = QtCore.Signal(str)

This works in both cases.

According to docs

Signals can be defined using the QtCore.Signal() class. Python types and C types can be passed as parameters to it. If you need to overload it just pass the types as tuples or lists.

https://wiki.qt.io/Signals_and_Slots_in_PySide#Using_QtCore.Signal.28.29