Derived classes receiving signals in wrong thread

2019-05-21 09:11发布

问题:

I'm having problems getting a derived class to receive signals properly in PySide. I'm using a transmitter and a receiver on two separate threads from the main (GUI or command-line application) thread. The threads are QThread objects. The transmitter and receiver are moved immediately after creation to their thread using QObject.moveToThread(). If the receiver is derived directly from QObject, all works fine, and the receiver receives within its thread. However, if the receiver is derived from a base class that is derived from QObject, the receiver still receives the signal, but does so on the wrong thread (the main thread).

Example (with some signal debugging code adapted from PyQt & unittest - Testing signal and slots):

#!/usr/bin/env python3
# weigh/bugtest_qt_signal_derived.py

import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
import sys
import threading
import time

from PySide import QtCore
from PySide.QtCore import (
    QCoreApplication,
    QObject,
    QThread,
    Signal,
    Slot,
)

_oldEmit = QtCore.QObject.emit  # normal method


def debug_emit(self, *args):
    logger.debug("EMIT: thread name={}, emit args={}".format(
        threading.current_thread().name,
        repr(args),
    ))
    _oldEmit(self, *args)

QtCore.QObject.emit = debug_emit


def report(msg):
    logger.info("{} [{}]".format(msg, threading.current_thread().name))


class Transmitter(QObject):
    transmit = Signal()
    finished = Signal()

    def start(self):
        count = 3
        logger.info("Starting transmitter")
        while count > 0:
            time.sleep(1)  # seconds
            report("transmitting, count={}".format(count))
            self.transmit.emit()
            count -= 1
        logger.info("Stopping transmitter")
        self.finished.emit()


class Base(QObject):
    def __init__(self, parent=None):
        super().__init__(parent=parent)

    @Slot()
    def start(self):
        report("Starting receiver")

    @Slot()
    def receive(self):
        report("receive: BASE")


class Derived(Base):
    def __init__(self, parent=None):
        super().__init__(parent=parent)

    @Slot()
    def receive(self):
        report("receive: DERIVED")


USE_DERIVED = True

if __name__ == '__main__':
    logging.basicConfig()
    logger.setLevel(logging.DEBUG)

    # Objects
    app = QCoreApplication(sys.argv)

    tx_thread = QThread()
    transmitter = Transmitter()
    transmitter.moveToThread(tx_thread)

    rx_thread = QThread()
    if USE_DERIVED:
        receiver = Derived()
    else:
        receiver = Base()
    receiver.moveToThread(rx_thread)

    # Signals: startup
    tx_thread.started.connect(transmitter.start)
    rx_thread.started.connect(receiver.start)
    # ... shutdown
    transmitter.finished.connect(tx_thread.quit)
    tx_thread.finished.connect(rx_thread.quit)
    rx_thread.finished.connect(app.quit)
    # ... action
    transmitter.transmit.connect(receiver.receive)

    # Go
    rx_thread.start()
    tx_thread.start()
    report("Starting app")
    app.exec_()

Output with USE_DERIVED = False:

INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [Dummy-1]
INFO:__main__:Starting transmitter
INFO:__main__:transmitting, count=3 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:receive: BASE [Dummy-1]
INFO:__main__:transmitting, count=2 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:receive: BASE [Dummy-1]
INFO:__main__:transmitting, count=1 [Dummy-2]
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2transmit()',)
INFO:__main__:Stopping transmitter
DEBUG:__main__:EMIT: thread name=Dummy-2, emit args=('2finished()',)
INFO:__main__:receive: BASE [Dummy-1]

Output with USE_DERIVED = True:

INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [MainThread]
INFO:__main__:Starting transmitter
INFO:__main__:transmitting, count=3 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: DERIVED [MainThread]
INFO:__main__:transmitting, count=2 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: DERIVED [MainThread]
INFO:__main__:transmitting, count=1 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:Stopping transmitter
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2finished()',)
INFO:__main__:receive: DERIVED [MainThread]

... the difference being that the Base class receives on its own thread, and the Derived class receives on the MainThread.

Does anyone know why? Many thanks!

Software: PySide version: 1.2.4; QtCore version: 4.8.6; Ubuntu 14.04; Python 3.4.4.

Further to @101's comment:

The signal override isn't necessary for failure. These derived classes also fail (in the sense of being called in the wrong thread):

class DerivedTwo(Base):
    def __init__(self, parent=None):
        super().__init__(parent=parent)


class DerivedThree(Base):
    def __init__(self, parent=None):
        QObject.__init__(self, parent=parent)

Since the output suggests the derived receiver object is starting on the wrong thread, I wondered if the problem is that QObject.moveToThread() fails for derived objects. However, that doesn't seem to be the case:

def debug_object(obj):
    logger.debug("Object {} belongs to QThread {}".format(obj, obj.thread()))

def debug_thread(thread_name, thread):
    logger.debug("{} is QThread {}".format(thread_name, thread))

# ...

tx_thread = QThread()
debug_thread("tx_thread", tx_thread)
transmitter = Transmitter()
debug_object(transmitter)
transmitter.moveToThread(tx_thread)
debug_object(transmitter)

rx_thread = QThread()
debug_thread("rx_thread", rx_thread)
receiver = DerivedTwo()
debug_object(receiver)
receiver.moveToThread(rx_thread)
debug_object(receiver)

gives

DEBUG:__main__:tx_thread is QThread <PySide.QtCore.QThread object at 0x7fc4a3befd08>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fc4a3bf2648> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2688>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fc4a3bf2648> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3befd08>
DEBUG:__main__:rx_thread is QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2708>
DEBUG:__main__:Object <__main__.DerivedTwo object at 0x7fc4a3bf2788> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2688>
DEBUG:__main__:Object <__main__.DerivedTwo object at 0x7fc4a3bf2788> belongs to QThread <PySide.QtCore.QThread object at 0x7fc4a3bf2708>
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting receiver [MainThread]
INFO:__main__:Starting transmitter [Dummy-1]
INFO:__main__:transmitting, count=3 [Dummy-1]
DEBUG:__main__:EMIT: thread name=Dummy-1, emit args=('2transmit()',)
INFO:__main__:receive: BASE [MainThread]
...

which suggests to me that the derived object gets transferred correctly to a new thread (notionally, for Qt events processing) during moveToThread(), but then is started on (and receives on) the main thread somehow.

Additional: it works in C++ Qt

Header:

// bugtest_qt_signal_derived.h

#include <QtCore/QCoreApplication>
#include <QtCore/QtDebug>  // not QDebug
#include <QtCore/QObject>
#include <QtCore/QString>  // works with qDebug where std::string doesn't
#include <QtCore/QThread>

void debug_object(const QString& obj_name, const QObject& obj);
void debug_thread(const QString& thread_name, const QThread& thread);
void report(const QString& msg);

class Transmitter : public QObject
{
    Q_OBJECT  // enables macros like "signals:", "slots:", "emit"
public:
    Transmitter() {}
    virtual ~Transmitter() {}
signals:
    void transmit();
    void finished();
public slots:
    void start();
};

class Base : public QObject
{
    Q_OBJECT
public:
    Base() {}
public slots:
    void start();
    void receive();
};

class Derived : public Base
{
    Q_OBJECT
public:
    Derived() {}
public slots:
    void receive();
};

Source:

// bugtest_qt_signal_derived.cpp

#include "bugtest_qt_signal_derived.h"
#include <unistd.h>  // for sleep()

void debug_object(const QString& obj_name, const QObject& obj)
{
    qDebug() << "Object" << obj_name << "belongs to QThread" << obj.thread();
}

void debug_thread(const QString& thread_name, const QThread& thread)
{
    qDebug() << thread_name << "is QThread at" << &thread;
}

void report(const QString& msg)
{
    qDebug().nospace() << msg << " [" << QThread::currentThreadId() << "]";
}

void Transmitter::start()
{
    unsigned int count = 3;
    report("Starting transmitter");
    while (count > 0) {
        sleep(1);  // seconds
        report(QString("transmitting, count=%1").arg(count));
        emit transmit();
        count -= 1;
    }
    report("Stopping transmitter");
    emit finished();
}

void Base::start()
{
    report("Starting receiver");
}

void Base::receive()
{
    report("receive: BASE");
}

void Derived::receive()
{
    report("receive: DERIVED");
}

#define USE_DERIVED

int main(int argc, char* argv[])
{
    // Objects
    QCoreApplication app(argc, argv);

    QThread tx_thread;
    debug_thread("tx_thread", tx_thread);
    Transmitter transmitter;
    debug_object("transmitter", transmitter);
    transmitter.moveToThread(&tx_thread);
    debug_object("transmitter", transmitter);

    QThread rx_thread;
    debug_thread("rx_thread", rx_thread);
#ifdef USE_DERIVED
    Derived receiver;
#else
    Base receiver;
#endif
    debug_object("receiver", receiver);
    receiver.moveToThread(&rx_thread);
    debug_object("receiver", receiver);

    // Signals: startup
    QObject::connect(&tx_thread, SIGNAL(started()),
                     &transmitter, SLOT(start()));    
    QObject::connect(&rx_thread, SIGNAL(started()),
                     &receiver, SLOT(start()));    
    // ... shutdown
    QObject::connect(&transmitter, SIGNAL(finished()),
                     &tx_thread, SLOT(quit()));    
    QObject::connect(&tx_thread, SIGNAL(finished()),
                     &rx_thread, SLOT(quit()));    
    QObject::connect(&rx_thread, SIGNAL(finished()),
                     &app, SLOT(quit()));    
    // ... action
    QObject::connect(&transmitter, SIGNAL(transmit()),
                     &receiver, SLOT(receive()));    

    // Go
    rx_thread.start();
    tx_thread.start();
    report("Starting app");
    return app.exec();
}

Output:

"tx_thread" is QThread at QThread(0x7ffc138c5330) 
Object "transmitter" belongs to QThread QThread(0xdae1e0) 
Object "transmitter" belongs to QThread QThread(0x7ffc138c5330) 
"rx_thread" is QThread at QThread(0x7ffc138c5350) 
Object "receiver" belongs to QThread QThread(0xdae1e0) 
Object "receiver" belongs to QThread QThread(0x7ffc138c5350) 
"Starting app" [0x7f032fb32780]
"Starting transmitter" [0x7f032ae77700]
"Starting receiver" [0x7f032b678700]
"transmitting, count=3" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]
"transmitting, count=2" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]
"transmitting, count=1" [0x7f032ae77700]
"Stopping transmitter" [0x7f032ae77700]
"receive: DERIVED" [0x7f032b678700]

Additional: It also works in PyQt

Code:

#!/usr/bin/env python2

import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
import sys
import threading
import time

from PyQt4.QtCore import (
    QCoreApplication,
    QObject,
    QThread,
    pyqtSignal,
    pyqtSlot,
)


def debug_object(obj):
    logger.debug("Object {} belongs to QThread {}".format(obj, obj.thread()))


def debug_thread(thread_name, thread):
    logger.debug("{} is QThread {}".format(thread_name, thread))


def report(msg):
    logger.info("{} [{}]".format(msg, threading.current_thread().name))


class Transmitter(QObject):
    transmit = pyqtSignal()
    finished = pyqtSignal()

    def start(self):
        count = 3
        report("Starting transmitter")
        while count > 0:
            time.sleep(1)  # seconds
            report("transmitting, count={}".format(count))
            self.transmit.emit()
            count -= 1
        report("Stopping transmitter")
        self.finished.emit()


class Base(QObject):
    def __init__(self, parent=None):
        super(Base, self).__init__(parent=parent)

    @pyqtSlot()
    def start(self):
        report("Starting receiver")

    @pyqtSlot()
    def receive(self):
        report("receive: BASE")


class Derived(Base):
    def __init__(self, parent=None):
        super(Derived, self).__init__(parent=parent)

    @pyqtSlot()
    def receive(self):
        report("receive: DERIVED")


USE_DERIVED = True

if __name__ == '__main__':
    logging.basicConfig()
    logger.setLevel(logging.DEBUG)

    # Objects
    app = QCoreApplication(sys.argv)

    tx_thread = QThread()
    debug_thread("tx_thread", tx_thread)
    transmitter = Transmitter()
    debug_object(transmitter)
    transmitter.moveToThread(tx_thread)
    debug_object(transmitter)

    rx_thread = QThread()
    debug_thread("rx_thread", rx_thread)
    if USE_DERIVED:
        receiver = Derived()
    else:
        receiver = Base()
    debug_object(receiver)
    receiver.moveToThread(rx_thread)
    debug_object(receiver)

    # Signals: startup
    tx_thread.started.connect(transmitter.start)
    rx_thread.started.connect(receiver.start)
    # ... shutdown
    transmitter.finished.connect(tx_thread.quit)
    tx_thread.finished.connect(rx_thread.quit)
    rx_thread.finished.connect(app.quit)
    # ... action
    transmitter.transmit.connect(receiver.receive)

    # Go
    rx_thread.start()
    tx_thread.start()
    report("Starting app")
    app.exec_()

Output:

DEBUG:__main__:tx_thread is QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad0770>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fd0b7ad0808> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
DEBUG:__main__:Object <__main__.Transmitter object at 0x7fd0b7ad0808> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad0770>
DEBUG:__main__:rx_thread is QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
DEBUG:__main__:Object <__main__.Derived object at 0x7fd0b7ad0938> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad09d0>
DEBUG:__main__:Object <__main__.Derived object at 0x7fd0b7ad0938> belongs to QThread <PyQt4.QtCore.QThread object at 0x7fd0b7ad08a0>
INFO:__main__:Starting app [MainThread]
INFO:__main__:Starting transmitter [Dummy-1]
INFO:__main__:Starting receiver [Dummy-2]
INFO:__main__:transmitting, count=3 [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]
INFO:__main__:transmitting, count=2 [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]
INFO:__main__:transmitting, count=1 [Dummy-1]
INFO:__main__:Stopping transmitter [Dummy-1]
INFO:__main__:receive: DERIVED [Dummy-2]

Confirm @101's findings in Python 3

Just as described below. All works fine just by removing all @Slot() decorators.

So it seems to be a PySide bug relating to the Slot decorator.

Many thanks!

回答1:

Using Python 2.7.10 and PySide 1.2.2 on Windows I made a similar example and found the same issue. And yes, when connecting to the derived class the code does actually seem to be stuck in the main thread (I checked this by blocking the main thread to show that the listener no longer responds). Here's the minimal example I used:

from PySide import QtCore, QtGui
import threading, time, sys

class Signaller(QtCore.QObject):
    signal = QtCore.Signal()
    def send_signals(self):
        while True:
            self.signal.emit()
            time.sleep(1)

class BaseListener(QtCore.QObject):
    @QtCore.Slot()
    def on_signal(self):
        print 'Got signal in', threading.current_thread().name

class DerivedListener(BaseListener):
    pass

class App(QtGui.QApplication):
    def __init__(self, sys_argv):
        super(App, self).__init__(sys_argv)

        # self.listener = BaseListener()
        self.listener = DerivedListener()
        self.listener_thread = QtCore.QThread()
        self.listener.moveToThread(self.listener_thread)

        self.signaller = Signaller()
        self.signaller_thread = QtCore.QThread()
        self.signaller.moveToThread(self.signaller_thread)
        self.signaller.signal.connect(self.listener.on_signal)
        self.signaller_thread.started.connect(self.signaller.send_signals)

        self.listener_thread.start()
        self.signaller_thread.start()

sys.exit(App(sys.argv).exec_())

I found several workarounds:

  • remove the @QtCore.Slot decorator from the base class (it's generally unnecessary anyway)
  • adding an unused argument to the base class's @QtCore.Slot decorator, e.g. @QtCore.Slot(int), but only if the argument is not actually passed as an argument to the method. Perhaps adding this dummy argument essentially invalidates the decorator.
  • use PyQt4

So yes, it seems that subclassing a class that already has a slot defined with a decorator cannot be properly moved to a thread. I'm also curious to know why exactly this is.

PySide bug here: https://bugreports.qt.io/browse/PYSIDE-249