信号槽可以说是我们使用QT时用得最多的功能,使用起来非常简单、方便,可能大多数人对它的直观理解是用来代替回调函数。也许最开始确实是这个目的,但是时至今日,它可以用来实现更多的功能,同时也有很多细节需要注意。
信号槽的注意点基本集中在connect函数的第五个参数上,吃透这第五个参数的所有标志位就能用它实现更多功能啦。同时的,你也能更了解QT的一些核心机制了,例如生存线程、事件循环及元对象。
因为我写文章的水平着实菜鸡,虽然很想让更多学QT的人少走一些弯路并逐渐壮大QT社区,但很多东西确实没法很好的表达出来,甚至很多东西需要花很长的时间去构思(还不一定好)。虽然在这篇文章之前大致介绍了生存线程和事件循环的概念,但是元对象还没介绍的(主要是憋不出来),所以看这篇文章的童鞋将看且看吧。
Qt::AutoConnection
这个标志位可能是QT信号槽被使用得最多的了,因为这个标志位是connect函数第五个参数的默认值。QT将这个标志作为默认值的主要原因应该是保证槽函数的执行线程一定和槽函数所在对象(connect函数第三个参数所传入的对象)的生存线程一致,即槽函数的执行线程一定是在槽函数所在对象的生存线程。正如同大多数人所理解的一样,当信号的发送线程(注意,这里不是connect第一个参数所传入的信号所在对象的生存线程,而是调用信号时的线程)和槽函数所在对象的生存线程一致时相当于Qt::DirectConnection,不一致时相当于Qt::QueuedConnection。因为它包含元对象相关的东西,我实在不知道该怎么介绍源码,感兴趣的可以自己了解一下相关源码。下面配合代码详细理解一下也够用了:
添加如下三个类
#pragma once
#include <QObject>
#include <QThread>
#include <QtDebug>
class SignalTest : public QObject
{
Q_OBJECT
public:
explicit SignalTest(QObject *parent = nullptr) : QObject(parent) {}
signals:
void signal_test1();
void signal_test2();
};
class SlotTest1 : public QObject
{
Q_OBJECT
public:
explicit SlotTest1(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void slot_test1()
{
qDebug() << "SlotTest1 slot_test1. cur thread:" << QThread::currentThread()
<< "live thread:" << thread();
}
void slot_test2()
{
qDebug() << "SlotTest1 slot_test2. cur thread:" << QThread::currentThread()
<< "live thread:" << thread();
}
};
class SlotTest2 : public QObject
{
Q_OBJECT
public:
explicit SlotTest2(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void slot_test3()
{
qDebug() << "SlotTest2 slot_test3. cur thread:" << QThread::currentThread()
<< "live thread:" << thread();
}
void slot_test4()
{
qDebug() << "SlotTest2 slot_test4. cur thread:" << QThread::currentThread()
<< "live thread:" << thread();
}
};
在mian函数中加入如下代码(注意pro文件中加入QT += concurrent)
#include "MainWindow.h"
#include <QApplication>
#include <QtConcurrent>
#include "SignalTest.h"
#include "SlotTest1.h"
#include "SlotTest2.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
QThread t;
t.start();
SignalTest signalTest1;
SignalTest signalTest2;
SlotTest1 slotTest1;
SlotTest2 slotTest2;
signalTest2.moveToThread(&t); // 将生存线程移动至新线程中
slotTest2.moveToThread(&t); // 将生存线程移动至新线程中
QObject::connect(&signalTest1, &SignalTest::signal_test1, &slotTest1, &SlotTest1::slot_test1, Qt::AutoConnection);
QObject::connect(&signalTest1, &SignalTest::signal_test2, &slotTest1, &SlotTest1::slot_test2, Qt::AutoConnection);
QObject::connect(&signalTest2, &SignalTest::signal_test1, &slotTest2, &SlotTest2::slot_test3, Qt::AutoConnection);
QObject::connect(&signalTest2, &SignalTest::signal_test2, &slotTest2, &SlotTest2::slot_test4, Qt::AutoConnection);
qDebug() << "signalTest1 signal_test1. cur thread:" << QThread::currentThread()
<< "live thread:" << signalTest1.thread();
emit signalTest1.signal_test1();
qDebug() << "signalTest2 signal_test1. cur thread:" << QThread::currentThread()
<< "live thread:" << signalTest2.thread();
emit signalTest2.signal_test1();
QtConcurrent::run([&]() {
qDebug() << "signalTest1 signal_test2. cur thread:" << QThread::currentThread()
<< "live thread:" << signalTest1.thread();
emit signalTest1.signal_test2();
qDebug() << "signalTest2 signal_test2. cur thread:" << QThread::currentThread()
<< "live thread:" << signalTest2.thread();
emit signalTest2.signal_test2();
});
int e = a.exec();
t.quit();
t.wait();
return e;
}
执行结果
signalTest1 signal_test1. cur thread: QThread(0x296d0bb07d0) live thread: QThread(0x296d0bb07d0) // signalTest1对象的signal_test1信号的发送线程和signalTest1对象的生存线程都为主线程
SlotTest1 slot_test1. cur thread: QThread(0x296d0bb07d0) live thread: QThread(0x296d0bb07d0) // 因为SlotTest1对象的生存线程和signalTest1一致且均为主线程,所以slot_test1槽函数在接收到信号后立即执行,并且槽的执行线程和生存线程一致
signalTest2 signal_test1. cur thread: QThread(0x296d0bb07d0) live thread: QThread(0xa73e0ff7e8) // 信号的发送线程和对象的生存线程不一致,发送线程为主线程,生存线程被移动到了其他线程
signalTest1 signal_test2. cur thread: QThread(0x296d2b689e0, name = "Thread (pooled)") live thread: QThread(0x296d0bb07d0) // 信号的发送线程和对象的生存线程不一致,发送线程为其他线程,生存线程为主线程
signalTest2 signal_test2. cur thread: QThread(0x296d2b689e0, name = "Thread (pooled)") live thread: QThread(0xa73e0ff7e8) // 信号的发送线程和对象的生存线程不一致,发送线程为其他线程,生存线程也为其他线程
SlotTest2 slot_test3. cur thread: QThread(0xa73e0ff7e8) live thread: QThread(0xa73e0ff7e8) // 因为和该槽函数连接的信号是在主线程发出的,所以该槽函数会延迟执行,且执行线程和生存线程一致
SlotTest2 slot_test4. cur thread: QThread(0xa73e0ff7e8) live thread: QThread(0xa73e0ff7e8) // 因为和该槽函数连接的信号是在其他线程发出的,所以该槽函数会延迟执行,且执行线程和生存线程一致
SlotTest1 slot_test2. cur thread: QThread(0x296d0bb07d0) live thread: QThread(0x296d0bb07d0) // 因为和该槽函数连接的信号是在其他线程发出的,所以该槽函数会延迟执行,且执行线程和生存线程一致
// 上面可能缺了一个例子,感兴趣可以自己试一下,我想应该能直接猜到结果了。将signalTest2对象的signal_test1函数在0xa73e0ff7e8线程中发送,那么SlotTest2对象的slot_test3槽函数也会立即执行,因为SlotTest2的生存线程也是0xa73e0ff7e8
根据上面的结果可以看到:
- 当信号的发送线程和槽函数所在对象的生存线程一致时,信号发送后槽函数立即执行,它是同步的。即槽函数slot_test1没有返回时,
emit signalTest1.signal_test1();
会一直处于阻塞状态,直到槽函数返回。 - 当信号的发送线程和槽函数所在对象的生存线程不一致时,信号发送后槽函数并没有立即执行,它是在槽函数所在对象的生存线程空闲时,由它的事件循环调用的,它是异步的。即信号发送后,无论槽函数有没有被执行,该信号直接返回并执行接下来的操作。
顺便提一句,当信号的发送线程和槽函数所在对象的生存线程不一致时,槽函数之所能在其生存线程中执行的原理是事件循环。在这种情况下其实是信号发送之后往槽函数所在对象的事件队列中抛了一个事件,再由事件循环调用QObject::event
函数所执行的,这也变相体现出事件循环的强大,在很多重要场景下几乎无处不在。
Qt::DirectConnection
将上面的Qt::AutoConnection
换成Qt::DirectConnection
再看执行结果
signalTest1 signal_test1. cur thread: QThread(0x2062c67b9c0) live thread: QThread(0x2062c67b9c0)
SlotTest1 slot_test1. cur thread: QThread(0x2062c67b9c0) live thread: QThread(0x2062c67b9c0)
signalTest2 signal_test1. cur thread: QThread(0x2062c67b9c0) live thread: QThread(0xbd32cff578)
SlotTest2 slot_test3. cur thread: QThread(0x2062c67b9c0) live thread: QThread(0xbd32cff578)
signalTest1 signal_test2. cur thread: QThread(0x2062e6c7680, name = "Thread (pooled)") live thread: QThread(0x2062c67b9c0)
SlotTest1 slot_test2. cur thread: QThread(0x2062e6c7680, name = "Thread (pooled)") live thread: QThread(0x2062c67b9c0)
signalTest2 signal_test2. cur thread: QThread(0x2062e6c7680, name = "Thread (pooled)") live thread: QThread(0xbd32cff578)
SlotTest2 slot_test4. cur thread: QThread(0x2062e6c7680, name = "Thread (pooled)") live thread: QThread(0xbd32cff578)
由上面的结果可以看到,槽函数的执行线程永远为信号的调用线程,和其生存线程无关。并且永远为同步执行,上一个槽函数返回后才会发送下一个
Qt::QueuedConnection
将上面的Qt::DirectConnection
换成Qt::QueuedConnection
再看执行结果
signalTest1 signal_test1. cur thread: QThread(0x24c41b50190) live thread: QThread(0x24c41b50190)
signalTest2 signal_test1. cur thread: QThread(0x24c41b50190) live thread: QThread(0xcbbacff9b8)
signalTest1 signal_test2. cur thread: QThread(0x24c43bc6680, name = "Thread (pooled)") live thread: QThread(0x24c41b50190)
signalTest2 signal_test2. cur thread: QThread(0x24c43bc6680, name = "Thread (pooled)") live thread: QThread(0xcbbacff9b8)
SlotTest2 slot_test3. cur thread: QThread(0xcbbacff9b8) live thread: QThread(0xcbbacff9b8)
SlotTest2 slot_test4. cur thread: QThread(0xcbbacff9b8) live thread: QThread(0xcbbacff9b8)
SlotTest1 slot_test1. cur thread: QThread(0x24c41b50190) live thread: QThread(0x24c41b50190)
SlotTest1 slot_test2. cur thread: QThread(0x24c41b50190) live thread: QThread(0x24c41b50190)
由上面的结果可以看到,槽函数的执行线程永远为其所在对象的生存线程,和信号的调用线程无关。并且永远为异步执行,无论上一个槽函数有没有被执行,下一个信号都会被发送。
注意:SlotTest2 slot_test3
一定先于SlotTest2 slot_test4
执行,它们俩的顺序是一定的,因为他们所在对象的生存线程一致。同理SlotTest1 slot_test1
和SlotTest1 slot_test2
顺序也是一定的。但是SlotTest2中的槽函数和SlotTest1中的槽函数之间的顺序不一定,因为它们俩生存线程不一致。
Qt::BlockingQueuedConnection
将上面的Qt::QueuedConnection
换成Qt::BlockingQueuedConnection
再看执行结果。
这里要注意:QObject::connect(&signalTest1, &SignalTest::signal_test1, &slotTest1, &SlotTest1::slot_test1, Qt::QueuedConnection);
这行代码不能改,如果你已经尝试改动并执行的话,会得到一个QT的线程死锁的提示Qt: Dead lock detected while activating a BlockingQueuedConnection: Sender is SignalTest(0x5afbdcf9f8), receiver is SlotTest1(0x5afbdcfb18)
。这是因为这个标志位内部会加锁,并且会等待槽函数执行完毕,但是信号的发送线程和槽函数所在对象的生存线程一致的时候,信号一致处于阻塞状态,槽函数也永远得不到执行所导致。
signalTest2 signal_test1. cur thread: QThread(0x228dddd2b90) live thread: QThread(0x7c930ff708)
SlotTest2 slot_test3. cur thread: QThread(0x7c930ff708) live thread: QThread(0x7c930ff708)
signalTest1 signal_test2. cur thread: QThread(0x228dfe54330, name = "Thread (pooled)") live thread: QThread(0x228dddd2b90)
SlotTest1 slot_test2. cur thread: QThread(0x228dddd2b90) live thread: QThread(0x228dddd2b90)
signalTest2 signal_test2. cur thread: QThread(0x228dfe54330, name = "Thread (pooled)") live thread: QThread(0x7c930ff708)
SlotTest2 slot_test4. cur thread: QThread(0x7c930ff708) live thread: QThread(0x7c930ff708)
由上面的结果可以看到,槽函数的执行线程一定为其所在对象的生存线程,并且槽函数是同步执行的。信号发送后,一定会等待槽函数执行完毕才会返回并发送下一个信号。
Qt::UniqueConnection
这个标志位使用得非常少,我这里不演示代码了,简单解释一下:
一个信号可以和同一个槽多次调用connect函数连接,如果连接了多次,那么槽函数也会执行多次。这个标志位的作用就是:如果一个信号已经和某一个槽函数连接过的话,再次链接时就会失败。使用方法为:QObject::connect(&signalTest1, &SignalTest::signal_test1, &slotTest1, &SlotTest1::slot_test1, Qt::AutoConnection | Qt::UniqueConnection);
最后提一句,如果标志位为Qt::DirectConnection
时,并且一个信号只连接了一个槽,那么信号的返回值是可以使用的,感兴趣可以研究一下,但用处不多。我在正在开发的orm框架中用到了这种方式。