Qt 4.8:来自不同线程的两个信号和一个插槽之间的连接行为

Posted

技术标签:

【中文标题】Qt 4.8:来自不同线程的两个信号和一个插槽之间的连接行为【英文标题】:Qt 4.8 : connection behavior between two signals and one slot from different threads 【发布时间】:2016-12-23 10:18:07 【问题描述】:

我有三个 QThread 运行三个事件循环。

在每个线程中,我有一个对象,这些对象有信号和槽。

    ThreadA => ObjectA 与 theSignalA 和 theSlotA ThreadB => ObjectB 与 theSignalB 和 theSlotB ThreadC => 带有信号 B 和插槽 B 的对象 C

我完全理解一个信号和一个插槽之间的连接是如何工作的:

// This connection would execute theSlotB in ThreadA
connect(objectA, SIGNAL(theSignalA()), objectB, SLOT(theSlotB()), Qt::DirectConnection);

// This connection would post an event in ThreadB event loop, which will execute theSlotB
connect(objectA, SIGNAL(theSignalA()), objectB, SLOT(theSlotB()), Qt::QueuedConnection);

我要问的是这样的事情的行为:

auto sig2sig = Qt::DirectConnection;
auto sig2slot = Qt::DirectConnection;
connect(objectA, SIGNAL(theSignalA()), objectB, SIGNAL(theSignalB()), sig2sig);
connect(objectB, SIGNAL(theSignalB()), objectC, SLOT(theSlotC()), sig2slot);

对于sig2sigsig2slot 的不同可能值,在theSlotC 执行的位置(和时间)。

sig2sig = DirectConnection, sig2slot= DirectConnection, 作为theSignalAtheSignalB 之间的直接连接? sig2sig = DirectConnection, sig2slot= QueuedConnection, 作为theSignalAtheSignalB 之间的QueuedConnection ? sig2sig = QueuedConnection, sig2slot= DirectConnection, theSlotC 是否在 ThreadB 中执行? sig2sig = QueuedConnection, sig2slot= QueuedConnection, theSlotC 是否在 ThreadC 中执行,但在重新发出来自 ThreadB 的信号的延迟之后?

或者也许信号/信号连接被丢弃了?

【问题讨论】:

连接的目标之间绝对没有区别:它们只是一种方法。该方法是由您编写的,还是由moc 编写的,都无关紧要。行为是相同的。您还应该注意,很少有理由不使用自动连接类型。 你能详细说明这几个原因吗? 如果您打算从事件循环中调用目标,即使它具有相同的线程上下文,您可能希望强制排队连接。如果目标方法是线程安全的,您可能希望强制直接连接到位于不同线程中的目标。您可能很少需要阻塞队列连接。就是这样。 【参考方案1】:

信号之间的连接类型确定发出第二个信号的线程,只需将信号视为执行它所连接的插槽的另一个函数/插槽(完全相同rules apply ):

如果类型为Qt::DirectConnection,则始终从发出第一个信号的线程发出第二个信号。 如果类型是Qt::QueuedConnection,第二个信号总是排队等待当控制返回到接收对象线程的事件循环时被调用。 如果类型为Qt::AutoConnection,则在发出信号时解析连接类型,忽略发送对象的线程。 如果接收器对象位于发出第一个信号的同一线程中,则这与使用 Qt::DirectConnection 相同。 否则,这将与使用Qt::QueuedConnection 相同。

我写了一个最小的测试来演示这个东西:

#include <QtCore>

//QThread wrapper for safe destruction
//see http://***.com/a/19666329
class Thread : public QThread
    using QThread::run; //final
public:
    Thread(QObject* parent= nullptr): QThread(parent)
    ~Thread() quit(); wait();
;

class Worker : public QObject
    Q_OBJECT
public:
    explicit Worker(QString name, QObject* parent= nullptr):QObject(parent)
        setObjectName(name);
        //the statement is printed from the thread that emits the signal
        //since we don't provide a context object
        connect(this, &Worker::workerSignal, [=]
            qDebug() << objectName() << "signal emitted from thread:"
                     << QThread::currentThread()->objectName();
        );
    
    ~Worker() = default;

    Q_SIGNAL void workerSignal();
    Q_SLOT void workerSlot()
        qDebug() << objectName() << "slot invoked in thread:"
                 << QThread::currentThread()->objectName();
    
;

int main(int argc, char* argv[])
    QCoreApplication a(argc, argv);

    //using the main thread as threadA
    QThread::currentThread()->setObjectName("threadA");
    Worker workerA("workerA");
    //creating threadB and threadC
    Thread threadB;
    threadB.setObjectName("threadB");
    Worker workerB("workerB");
    workerB.moveToThread(&threadB);
    Thread threadC;
    threadC.setObjectName("threadC");
    Worker workerC("workerC");
    workerC.moveToThread(&threadC);
    threadB.start(); threadC.start();

    //change the following types to whatever case you want to test:
    auto sig2sig= Qt::QueuedConnection;
    auto sig2slot= Qt::QueuedConnection;
    qDebug() << "sig2sig= " << sig2sig << ", sig2slot=" << sig2slot;
    QObject::connect(&workerA, &Worker::workerSignal,
                     &workerB, &Worker::workerSignal, sig2sig);
    QObject::connect(&workerB, &Worker::workerSignal,
                     &workerC, &Worker::workerSlot, sig2slot);
    emit workerA.workerSignal();

    //quit application after 0.5 second
    QTimer::singleShot(500, &a, &QCoreApplication::quit);
    return a.exec();


#include "main.moc"

这将设置如下连接:

workerA::workerSignal() -------> workerB::workerSignal() -------> workerC::workerSlot()

每个工作线程都存在于自己的线程中,您可以通过更改分配给sig2sigsig2slot 变量的值来更改连接类型。这是您要求的情况下的输出:

sig2sig = DirectConnectionsig2slot= DirectConnection

线程A中的所有内容都作为直接函数调用执行。

"workerA" signal emitted from thread: "threadA"
"workerB" signal emitted from thread: "threadA"
"workerC" slot invoked in thread: "threadA"

sig2sig = DirectConnectionsig2slot= QueuedConnection

信号在threadA 中作为直接函数调用执行。插槽在threadC 中调用。

"workerA" signal emitted from thread: "threadA"
"workerB" signal emitted from thread: "threadA"
"workerC" slot invoked in thread: "threadC"

sig2sig = QueuedConnectionsig2slot= DirectConnection

信号已排队并从threadB 发出。该插槽在threadB 中作为直接函数调用进行调用。

"workerA" signal emitted from thread: "threadA"
"workerB" signal emitted from thread: "threadB"
"workerC" slot invoked in thread: "threadB"

sig2sig = QueuedConnectionsig2slot= QueuedConnection

信号已排队并从threadB 发出。插槽调用也排队并在threadC 中执行。所以,每件事都发生在 right 线程中,如果使用Qt::AutoConnection,这将是相同的行为:

"workerA" signal emitted from thread: "threadA"
"workerB" signal emitted from thread: "threadB"
"workerC" slot invoked in thread: "threadC"

【讨论】:

【参考方案2】:

在 qt 中,信号只是另一个函数,当被调用时,它会通过请求的方法调用槽,即

DirectConnection -> 在当前线程中调用槽, QueuedConnection -> 参数被复制,并且下次进入事件循环时在目标线程中调用槽, AutoConnection -> 如果信号在目标槽的线程中发出,则直接调用槽,否则排队。

当您将信号作为插槽连接时,它的行为类似于插槽,即通过请求的方法调用/发出。请注意,实际发射是线程安全的,因此只要插槽通过 AutoConnection 或 QueuedConnection 连接(请参阅Signals and Slots Across Threads),就可以从任何线程直接调用/发射信号。

一些例子:

sig2sig = DirectConnection, sig2slot= DirectConnection

当前线程 (A) 中调用的所有内容。插槽需要是线程安全的。

sig2sig = DirectConnection, sig2slot= QueuedConnection

signalB 在线程 A 中发出,slotC 已正确排队在自己的线程中执行。 同 sig2sig = DirectConnection, sig2slot= AutoConnection

sig2sig = QueuedConnection, sig2slot= QueuedConnection

signalB 排队等待在线程 B 中发出。当这种情况发生时,slotC 排队等待在线程 C 中执行。 与 sig2sig = AutoConnection, sig2slot= AutoConnection 相同。

结论

由于排队连接比直接连接慢得多,如果这是一个瓶颈,最好将 sig2sig 与 DirectConnection 连接并记录 DirectConnections 到 signalB 必须是线程安全的。

但是,如果只有两个线程,您应该使用 AutoConnection。那么只能有一个排队的呼叫,而另一个将是直接的。

【讨论】:

【参考方案3】:

如果您查看在 MOC 的对象中,您会看到每个信号都有一个信号处理程序方法,该方法将激活(例如执行或发送事件)连接的插槽,当您发出信号时将调用此方法,所以发射实际上在发射的线程中调用该对象的方法。实际上 is 信号处理程序只是另一个插槽。在信号到信号连接的情况下,第一个对象的信号处理程序将在调用线程中被调用并激活连接线程中的插槽(例如第二个对象的信号处理程序)。激活的槽可以是目标对象的槽或其信号之一。激活确实关心发射器信号和目标插槽/信号之间的连接类型。当您将一个信号连接到另一个信号时,实际上您是在将一个信号连接到第二个对象的信号处理程序。

在 DirectConnection 中,插槽将在发射器线程中调用。 在 QueuedConnection 中,插槽将在目标对象的线程中被调用。

因此,如果您将目标信号想象为另一个发出目标信号的插槽,实际上我们将拥有:

sig2sig=直接,sig2slot=直接

SignalA和SignalB是直接连接的,所以ObjectB的signal handler会在ObjectA的线程中执行,SignalB和SlotC的连接也是直接的,所以SlotC会在ObjectA的线程emiter线程中执行

sig2sig=直接,sig2slot=排队

SignalA和SignalB直接连接,ObjectB的signal handler会在ObjectA的线程中执行,而SignalB和SlotC的连接是排队的,所以SlotC会在ObjectC的线程中执行

sig2sig=排队,sig2slot=直接

SignalA 和 SignalB 之间存在排队连接,因此 SignalA 的发射将执行 ObjectB 的信号处理程序 int ObjectB 的线程,现在 ObjectB 的信号处理程序将以 Direct 方案激活 SlotC,因此 SlotC 将在其发射器线程中执行是 ObjectB 的线程(Thread B)。

sig2Sig=已排队,sig2slot=已排队

SignalA 和 SignalB 以及 SignalB 和 SlotC 之间存在排队连接,因此通过发出 SignalA,ObjectB 的信号处理程序将在 ObjectB 的线程 (ThreadB) 中调用,并发出 SignalB,该 SignalB 在 ObjectC 的线程 (ThreadC) 中调用 SlotC

这段代码也验证了行为

#include <QObject>
#include <QDebug>
#include <QThread>

class XClass : public QObject

    Q_OBJECT

public:
    XClass(char ident)  this->ident=ident; this->moveToThread(&this->t); this->t.start(); 
    char ident;
private:
        QThread t;
signals:
    void signalX();

public slots:
    void printTid()  qDebug() << "Object " << this->ident << " lives in thread" << QThread::currentThreadId(); 
    void slotX()  qDebug() << "Slot" << this->ident << " fired in thread" << QThread::currentThreadId(); 
    void emitSignalX() qDebug() << "Signal" << this->ident << "emitng from thread" << QThread::currentThreadId(); emit signalX(); 
;



int main(int argc, char *argv[])

    QCoreApplication a(argc, argv);

    XClass A('A');
    XClass B('B');
    XClass C('C');

    QMetaObject::invokeMethod(&A, "printTid", Qt::BlockingQueuedConnection);
    QMetaObject::invokeMethod(&B, "printTid", Qt::BlockingQueuedConnection);
    QMetaObject::invokeMethod(&C, "printTid", Qt::BlockingQueuedConnection);

    QObject::connect(&A, &XClass::signalX, &B, &XClass::signalX, Qt::DirectConnection);
    QObject::connect(&B, &XClass::signalX, &C, &XClass::slotX, Qt::DirectConnection);

    //QObject::connect(&A, &XClass::signalX, &B, &XClass::signalX, Qt::DirectConnection);
    //QObject::connect(&B, &XClass::signalX, &C, &XClass::slotX, Qt::QueuedConnection);

    //QObject::connect(&A, &XClass::signalX, &B, &XClass::signalX, Qt::DirectConnection);
    //QObject::connect(&B, &XClass::signalX, &C, &XClass::slotX, Qt::QueuedConnection);

    //QObject::connect(&A, &XClass::signalX, &B, &XClass::signalX, Qt::QueuedConnection);
    //QObject::connect(&B, &XClass::signalX, &C, &XClass::slotX, Qt::QueuedConnection);

    QMetaObject::invokeMethod(&A, "emitSignalX");

    return a.exec();

【讨论】:

以上是关于Qt 4.8:来自不同线程的两个信号和一个插槽之间的连接行为的主要内容,如果未能解决你的问题,请参考以下文章

在 QObject 之间跨不同线程连接信号/插槽

两个不同类别的信号和插槽

Qt 插槽同时断开连接并从不同线程调用

Qt 信号和插槽发送结构数组

Qt - 如何在不显式实现线程的情况下同时运行两个插槽

Qt插槽和信号:MainWindow中没有匹配的功能