PySide(Qt/PyQt)中派生类在错误线程中接收信号

4
我在PySide中遇到了问题,无法让派生类正确接收信号。我在主(GUI或命令行应用程序)线程和两个单独的线程上使用一个发射器和一个接收器。这些线程是QThread对象。在创建后立即使用QObject.moveToThread() 将发射器和接收器移动到它们的线程中。如果接收器直接派生自QObject,则一切正常,接收器在其线程内接收到信号。但是,如果接收器派生自派生自QObject的基类,则接收器仍会接收到信号,但是会在错误的线程(主线程)中接收到信号。 示例(包含一些从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_()

使用 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]

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]

...不同之处在于,Base类在其自己的线程上接收,而Derived类在MainThread上接收。

有人知道为什么吗?非常感谢!

软件:PySide版本:1.2.4; QtCore版本:4.8.6; Ubuntu 14.04; Python 3.4.4。

进一步说明 @101 的评论:

信号重载对于失败并不是必要的。这些派生类也会失败(在被调用的线程错误的情况下):

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)

由于输出表明派生接收器对象正在错误的线程上启动,我想知道问题是否在于QObject.moveToThread()对于派生对象失败。然而,事实并非如此:

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)

提供

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]
...

这让我认为在 moveToThread()期间,派生对象能够正确地转移到一个新线程(理论上,用于Qt事件处理),但随后在主线程上启动并接收。

附加信息:此方法适用于C++ Qt。

头文件:

// 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();
};

来源:

// 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();
}

输出:

"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]

附加信息:它也可以在PyQt中使用

代码:

#!/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_()

输出:

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]

在Python 3中确认@101的发现

就像下面所描述的那样。只需删除所有@Slot()修饰符即可正常工作。

因此,这似乎是与Slot修饰符相关的PySide bug。

非常感谢!


如果Derived中的receive没有覆盖Base中的receive,这会有什么区别吗? - 101
谢谢 - 更多信息已添加到上面的问题中。 - Rudolf Cardinal
由于@101的回答,我现在也发现了一个关于此问题的公开错误报告:https://bugreports.qt.io/browse/PYSIDE-249。谢谢! - Rudolf Cardinal
1个回答

4

使用Python 2.7.10和PySide 1.2.2在Windows上,我制作了一个类似的示例,并发现了相同的问题。是的,当连接到派生类时,代码似乎确实卡在主线程中(我通过阻塞主线程来检查这一点,以展示监听器不再响应)。以下是我使用的最小示例:

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_())

我找到了几个解决方法:
  • 从基类中删除@QtCore.Slot装饰器(通常是不必要的)
  • 向基类的@QtCore.Slot装饰器添加一个未使用的参数,例如@QtCore.Slot(int),但前提是该参数实际上没有作为方法的参数传递。也许添加这个虚拟参数会使装饰器无效。
  • 使用PyQt4
所以,是的,似乎将已经使用装饰器定义了槽的类作为子类无法正确地移动到线程中。我也很想知道为什么会出现这种情况。 这里是PySide的错误:https://bugreports.qt.io/browse/PYSIDE-249

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