1+1=10

扬长避短 vs 取长补短

重看Qt中连接到同一signal的多个slots的执行顺序问题

十多年前基于Qt4简单写过 Qt中连接到同一signal的多个slots的执行顺序问题,或许是时候再回顾一下了...

首先,结论没有变:

in the order they have been connected

只是本次稍微扩充一下,同时将内容从Qt4更新到Qt6

  • 先看Qt手册【中的片段】
  • 用十个简短的例子进行演示说明
  • 再看Qt源码【中的片段】

Qt手册

学习Qt,还是建议手册优先。

在Qt6.5手册中

  • https://doc.qt.io/qt-6.5/signalsandslots.html:

If several slots are connected to one signal, the slots will be executed one after the other,

  • https://doc.qt.io/qt-6.5/qobject.html

If a signal is connected to several slots, the slots are activated in the same order in which the connections were made, when the signal is emitted.

需要注意的是,一些老的书籍或资料中,认为槽函数的执行顺序是随机的。

这是因为在Qt4.5以及之前版本中,Qt官方是这么说的:

If several slots are connected to one signal, the slots will be executed one after the other, in an arbitrary order, when the signal is emitted.

If a signal is connected to several slots, the slots are activated in an arbitrary order when the signal is emitted.

个人理解,这个顺序一直是确定的,只是早期官方不想让用户依赖这个顺序。

手册中的这一变化,是从Qt4.6开始的。另外,Qt::UniqueConnection 这一个连接类型,也是在Qt4.6引入的。

另外,Qt6.0 引入新的连接类型 Qt::SingleShotConnection。

全家福:

| 枚举量 | 值 | 描述 | | ---------------------------- | ----- | ------------------------------------------------------------ | | Qt::AutoConnection | 0 | 这个是默认值。 如果接受者位于emit所在线程,则使用Qt::DirectConnection,否则使用Qt::QueuedConnection | | Qt::DirectConnection | 1 | 基础类型,等同于直接调用 | | Qt::QueuedConnection | 2 | 基础类型,发送一个事件到接收者所在线程的事件队列,接收者线程处理到该事件时,调用对应的槽函数。 | | Qt::BlockingQueuedConnection | 3 | 与Qt::QueuedConnection类似,emit所在线程会被阻塞 | | Qt::UniqueConnection | 0x80 | Qt4.6 引入,可与上面类型进行”与“操作。 | | Qt::SingleShotConnection | 0x100 | Qt6.0 引入,可与上面类型进行”与“操作。 |

执行顺序演示

我们还是以具体的例子开始。

为了简单起见,每个例子均只有一个main.cpp文件构成,可用cmake或qmake构建。

例子一

简单场景,使用 Qt::AutoConnection 模式,单线程模式:

```cpp // By dbzhang800, 2023-12-02

include

include

include

class MyObject : public QObject { Q_OBJECT public: MyObject() {}

signals: void dbSignal();

public slots: void dbSlot1() { qDebug() << QString("From %1 slot1: ").arg(objectName()) << QThread::currentThreadId(); } void dbSlot2() { qDebug() << QString("From %1 slot1: ").arg(objectName()) << QThread::currentThreadId(); } void dbSlot3() { qDebug() << QString("From %1 slot3: ").arg(objectName()) << QThread::currentThreadId(); } };

int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2);

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下(两个槽函数按连接顺序依次执行):

From main thread: 0x8468 "From slot1: " 0x8468 "From slot2: " 0x8468

例子二

为了节省篇幅,后面的例子只包含main函数,其他部分和例子一一样(其实每个例子只有几行代码的差异)。

简单场景,使用 Qt::AutoConnection 模式,多线程模式(2个线程):

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2);

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下(两个槽函数按连接顺序依次执行,从线程ID可以看出是在次线程执行):

From main thread: 0x9058 "From slot1: " 0x8f00 "From slot2: " 0x8f00

槽函数在同一个线程中执行,emit时会放置两个事件(event)进目标线程的事件队列。目标线程依次取出并执行它们,很自然。

注意

本例中,刻意将线程操作放置到connect之后。用以展示:

AutoConnection 到底等同于 DirectConnection还是QueuedConnection,是在emit时确定的,而不是connect时!!

例子三

直接使用 Qt::DirectConnection 和 Qt::QueuedConnection,多线程模式(2个线程):

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::QueuedConnection); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2, Qt::DirectConnection);

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

如果直接运行的话,你看到的运行结果应该如下:

From main thread: 0x759c "From slot2: " 0x759c "From slot1: " 0x89e4

什么状况?说好的按顺序执行呢?!!怎么slot2抢跑了

其实理解偏差在"执行"上。对Queued方式,有两个层次的”执行“:

  1. emit信号时,将需要执行的槽函数信息,封装成一个event,丢到目标线程的事件队列中。这个时序是确定的
  2. 目标线程依次处理队列中的事件。何时处理并执行这个槽函数,对于emit线程来说,这个时间是不可控的(也不该控制)。

槽函数依次执行,只涵盖第一层次。

注意

上面结果是不严谨的。slot1和slot2位于两个不同线程中。具体谁先谁后是不确定的,也不能通过上面的实验测定(因为一般来说,Queued方式肯定会慢,毕竟目标线程需要发现它并执行)

只要这么改一下,就可以发现问题(如果你不能复现,请把100继续改大):

cpp QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::QueuedConnection); for (int i = 0; i< 100; ++i) QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2, Qt::DirectConnection);

结果

From main thread: 0x77b4 "From slot2: " 0x77b4 "From slot2: " 0x77b4 ... "From slot2: " 0x77b4 "From slot1: " 0x982c "From slot2: " 0x77b4 ...

例子四

如果就是接受不了三的结果,非要同步怎么办?

改变一行代码,引入 Qt::BlockingQueuedConnection

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::BlockingQueuedConnection); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2, Qt::DirectConnection);

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下:

From main thread: 0x6d44 "From slot1: " 0x9b2c "From slot2: " 0x6d44

现在舒服多了,唯一问题是,这会阻塞当前线程

例子五

继续看看 Qt::BlockingQueuedConnection

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot3, Qt::BlockingQueuedConnection);

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下:

From main thread: 0x3634 "From slot1: " 0x43fc "From slot2: " 0x43fc "From slot3: " 0x43fc

在emit发生时,它会依次把 slot1、slot2、slot3对应的事件放置到目标线程的事件队列中,然后等待slot3的事件被去除并执行完毕。

这造成所有的槽函数都会阻塞当前线程,尽管执行顺序符合预期。

例子六

考虑真实的多线程场景,如下,我们使用两个工作线程:

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj1; obj1.setObjectName("obj1"); MyObject obj2; obj2.setObjectName("obj2"); QObject::connect(&obj1, &MyObject::dbSignal, &obj1, &MyObject::dbSlot1); QObject::connect(&obj1, &MyObject::dbSignal, &obj2, &MyObject::dbSlot1);

QThread thread1; obj1.moveToThread(&thread1); thread1.start(); QThread thread2; obj2.moveToThread(&thread2); thread2.start();

//thread.wait(1000);

emit obj1.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果有时:

From main thread: 0x83c8 "From obj1 slot1: " 0x3540 "From obj2 slot1: " 0x636c 有时: From main thread: 0x1818 "From obj2 slot1: " 0x9024 "From obj1 slot1: " 0x7984

同样,回归到我们例子三的解释中:

对Queued方式,有两个层次的”执行“:

  1. emit信号时,将需要执行的槽函数信息,封装成一个event,丢到目标线程的事件队列中。这个时序是确定的
  2. 目标线程依次处理队列中的事件。何时处理并执行这个槽函数,对于emit线程来说,这个时间是不可控的(也不该控制)。

槽函数依次执行,只涵盖第一层次。而第二层次的不确定,这刚好是多线程的特质。

其他选项

例子七

在例子三中,我们顺便演示了,同一个信号和槽之间可以多次建立连接,效果是槽函数执行多次。

那么,如何避免这种问题??只想执行一次怎么办??

这就需要 Qt::UniqueConnection 出场了...

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::QueuedConnection); for (int i = 0; i< 100; ++i) { QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot2, static_cast(Qt::DirectConnection|Qt::UniqueConnection)); }

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下:

From main thread: 0x1844 "From slot2: " 0x1844 "From slot1: " 0x8820

从结果看,重复连接被干掉了。

例子八

新的例子,继续看 Qt::UniqueConnection

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::DirectConnection); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, static_cast(Qt::QueuedConnection|Qt::UniqueConnection)); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, static_cast(Qt::DirectConnection|Qt::UniqueConnection));

QThread thread; obj.moveToThread(&thread); thread.start();

emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下:

From main thread: 0x4f0 "From slot1: " 0x4f0

从这儿可以发现,Qt::UniqueConnection 这个选项,是在connect时生效的。不管之前已有连接是什么类型,只要存在对应连接,当前connect就会被忽略。

注意

信号槽连接的新老用法混用,会不符合预期,比如,这个例子中,我们改成下面这样:

cpp QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::DirectConnection); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, static_cast<Qt::ConnectionType>(Qt::QueuedConnection|Qt::UniqueConnection)); QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, static_cast<Qt::ConnectionType>(Qt::DirectConnection|Qt::UniqueConnection)); QObject::connect(&obj, SIGNAL(dbSignal()), &obj, SLOT(dbSlot1()), static_cast<Qt::ConnectionType>(Qt::DirectConnection|Qt::UniqueConnection));

运行结果将为

From main thread: 0x82f4 "From slot1: " 0x82f4 "From slot1: " 0x82f4

也就是最后一行没起到UniqueConnection作用。

例子九

看看Qt6引入的,Qt::SingleShotConnection的选项

```cpp int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); qDebug() << "From main thread: " << QThread::currentThreadId();

MyObject obj; QObject::connect(&obj, &MyObject::dbSignal, &obj, &MyObject::dbSlot1, Qt::SingleShotConnection);

emit obj.dbSignal(); emit obj.dbSignal(); emit obj.dbSignal();

return a.exec(); }

include "main.moc"

```

运行结果如下:

From main thread: 0x4f0 "From slot1: " 0x4f0

不管信号触发多少次,槽函数第一次被调用后,这个链接就断开(disconnect)了。

注意对比:Qt::UniqueConnection效力发生在connect方式时,Qt::SingleShotConnection效力发生在emit执行时。

例子十

最后,尽管不追求十全十美,还是凑个数凑到十吧。看一下使用Qt designer时,非常常用(或者被动无意识在用)的信号槽自动连接,即QMetaObject::connectSlotsByName()介入进来会怎么样。

```cpp // By dbzhang800, 2023-12-02

include

include

include

class MyObject2 : public QObject { Q_OBJECT public: MyObject2() {setObjectName("obj2");}

signals: void dbSignal();

public slots: void on_obj2_dbSignal() { qDebug() << QString("From %1 slot1: ").arg(objectName()) << QThread::currentThreadId(); } };

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

MyObject2 obj2; QMetaObject::connectSlotsByName(&obj2);

QObject::connect(&obj2, &MyObject2::dbSignal, &obj2, &MyObject2::on_obj2_dbSignal); QObject::connect(&obj2, SIGNAL(dbSignal()), &obj2, SLOT(on_obj2_dbSignal()));

emit obj2.dbSignal();

return a.exec(); } ```

运行结果如下:

"From obj2 slot1: " 0x3b18 "From obj2 slot1: " 0x3b18 "From obj2 slot1: " 0x3b18

自动连接,加上两次手动连接,共有三个connection。故而调用了三次。

在例子八中,我们提到信号槽新老写法不等价。那么connectSlotsByName()这个Qt4时代的产物,和那个连接等价呢?

用下面的写法即可验证你的猜想。

cpp QObject::connect(&obj2, &MyObject2::dbSignal, &obj2, &MyObject2::on_obj2_dbSignal); QObject::connect(&obj2, SIGNAL(dbSignal()), &obj2, SLOT(on_obj2_dbSignal()), Qt::UniqueConnection);

源码

知其然,也要知其所以然。要不?简单看看Qt源码?

保存connection信息

QObject的connect函数会调用到下面这个函数来保存当前连接信息:

cpp QObjectPrivate::Connection *QMetaObjectPrivate::connect(const QObject *sender, int signal_index, const QMetaObject *smeta, const QObject *receiver, int method_index, const QMetaObject *rmeta, int type, int *types) { ... QObjectPrivate::ConnectionData *scd = QObjectPrivate::get(s)->connections.loadRelaxed(); if (type & Qt::UniqueConnection && scd) { if (scd->signalVectorCount() > signal_index) { const QObjectPrivate::Connection *c2 = scd->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed(); int method_index_absolute = method_index + method_offset; while (c2) { if (!c2->isSlotObject && c2->receiver.loadRelaxed() == receiver && c2->method() == method_index_absolute) return nullptr; c2 = c2->nextConnectionList.loadRelaxed(); } } } type &= ~Qt::UniqueConnection; const bool isSingleShot = type & Qt::SingleShotConnection; type &= ~Qt::SingleShotConnection; ... std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection}; c->sender = s; c->signal_index = signal_index; c->receiver.storeRelaxed( r); QThreadData *td = r->d_func()->threadData.loadAcquire(); td->ref(); c->receiverThreadData.storeRelaxed( td); c->method_relative = method_index; c->method_offset = method_offset; c->connectionType = type; c->isSlotObject = false; c->argumentTypes.storeRelaxed( types); c->callFunction = callFunction; c->isSingleShot = isSingleShot; QObjectPrivate::get(s)->addConnection(signal_index, c.get()); ... }

这个函数做了大量的判断,并过滤掉UniqueConnection,生成一个Connection,将其放置到内部的链表中

信号触发

首先,我们知道Qt中所谓signal,就是一个普通的函数,只是不用我们自己去实现它。moc会自动生成一个 moc_xxxx.cpp或者xxxx.moc文件,里面包含这个信号的实现。这样,C++编译器才能正常编译Qt程序,这部分可以参考从C++到Qt

对我们前面的例子中,打开 main.moc,可以看到下面的代码:

cpp // SIGNAL 0 void MyObject::dbSignal() { QMetaObject::activate(this, &staticMetaObject, 0, nullptr); }

信号触发(调用信号函数时)时,activate进而会调用如下的函数:

```cpp template void doActivate(QObject sender, int signal_index, void argv) { .... QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed()); QObjectPrivate::SignalVector signalVector = connections->signalVector.loadRelaxed(); const QObjectPrivate::ConnectionList *list; if (signal_index < signalVector->count()) list = &signalVector->at(signal_index); else list = &signalVector->at(-1); ....

uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
do {
    QObjectPrivate::Connection *c = list->first.loadRelaxed();
    if (!c)
        continue;
    do {
    ....
        // determine if this connection should be sent immediately or
        // put into the event queue
        if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
            || (c->connectionType == Qt::QueuedConnection)) {
            queued_activate(sender, signal_index, c, argv);
            continue;

if QT_CONFIG(thread)

        } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            if (receiverInSameThread) {
                qWarning(

"Qt: Dead lock detected while activating a BlockingQueuedConnection: " "Sender is %s(%p), receiver is %s(%p)", sender->metaObject()->className(), sender, receiver->metaObject()->className(), receiver); } if (c->isSingleShot && !QObjectPrivate::removeConnection(c)) continue; QSemaphore semaphore; { QMutexLocker locker(signalSlotLock(receiver)); if (!c->isSingleShot && !c->receiver.loadAcquire()) continue; QMetaCallEvent *ev = c->isSlotObject ? new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) : new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, argv, &semaphore); QCoreApplication::postEvent(receiver, ev); } semaphore.acquire(); continue;

endif

        }
        if (c->isSingleShot && !QObjectPrivate::removeConnection(c))
            continue;            
       QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);
        if (c->isSlotObject) {
            SlotObjectGuard obj{c->slotObj};
            {
                obj->call(receiver, argv);
            }
        } else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
            //we compare the vtable to make sure we are not in the destructor of the object.
            const int method_relative = c->method_relative;
            const auto callFunction = c->callFunction;
            const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
            callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
        } else {
            const int method = c->method_relative + c->method_offset;
            QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
        }        
    ....
    } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
} while (list != &signalVector->at(-1) && ((list = &signalVector->at(-1)), true));

```

这个函数很长:

  • 找到信号对应的connection列表
  • 遍历列表
  • 对于Queued连接,调用queued_activate()处理
  • 对于BlockingQueued连接,发送QMetaCallEvent事件,使用信号量 QSemaphore并等待其释放。
  • 其他,分情况调用

对于queued_activate来说,主要就是生成并发送QMetaCallEvent事件到接收者线程:

cpp static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv) { const int *argumentTypes = c->argumentTypes.loadRelaxed(); .... if (argumentTypes == &DIRECT_CONNECTION_ONLY) // cannot activate return; .... QMetaCallEvent *ev = c->isSlotObject ? new QMetaCallEvent(c->slotObj, sender, signal, nargs) : new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs); .... if (c->isSingleShot && !QObjectPrivate::removeConnection(c)) { delete ev; return; } .... QCoreApplication::postEvent(receiver, ev); }

当然,这个事件,最终会到事件处理函数中

cpp bool QObject::event(QEvent *e) { ... case QEvent::MetaCall: { QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e); if (!d_func()->connections.loadRelaxed()) { QMutexLocker locker(signalSlotLock(this)); d_func()->ensureConnectionData(); } QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId()); mce->placeMetaCall(this); break; } ...

再展开一下

cpp void QMetaCallEvent::placeMetaCall(QObject *object) { if (d.slotObj_) { d.slotObj_->call(object, d.args_); } else if (d.callFunction_ && d.method_offset_ <= object->metaObject()->methodOffset()) { d.callFunction_(object, QMetaObject::InvokeMetaMethod, d.method_relative_, d.args_); } else { QMetaObject::metacall(object, QMetaObject::InvokeMetaMethod, d.method_offset_ + d.method_relative_, d.args_); } }

而前面提到的BlockingQueued方式,用到的信号量,是在这儿释放的:

```cpp QAbstractMetaCallEvent::~QAbstractMetaCallEvent() {

if QT_CONFIG(thread)

if (semaphore_)
    semaphore_->release();

endif

} ```

参考

  • https://woboq.com/blog/how-qt-signals-slots-work.html
  • https://woboq.com/blog/how-qt-signals-slots-work-part3-queuedconnection.html

Qt qt, c++