Qt篇: 信号槽的连接方式

信号槽的连接方式

在没有接触多线程之前,想必大家对于信号槽的连接connect第五个参数都是默认值的使用,其实这个第五个参数才是信号槽连接最重要的,Qt的强大之处,就是提供了对象与对象之间以信号槽的方式进行交互,而第五个参数则是关系到了多线程之间的通讯关系。

第五个参数决定槽函数调用时候的相关行为,简单来说是决定由哪个线程对象去调用!

信号槽连接方式在Qt 5.15之前是有五种连接方式,在Qt 5.15新增了第六种连接方式Qt::SingleShotConnection:

  • Qt::AutoConnect(默认连接);
  • Qt::DirectConnection(立即调用);
  • Qt::QueuedConnection(异步调用);
  • Qt::BlockingQueuedConnection(同步调用)
  • Qt::UniqueConnection(单一连接);
  • Qt::SingleShotConnection(Qt 5.15 引入,一次性连接)

连接方式举例

连接方式我们主要研究前五个即可。在了解这些连接方式之前,我们需要知道下面的知识点,对于了解连接方式会迎刃而解。

每个线程都有属于自身的事件队列,线程通过事件队列来接收信号,信号在事件循环中被处理! Qt多线程信号槽和事件队列关系

从图中可以知道,线程A发射信号signal信号到线程B的事件队列中,假设一旦线程B开启事件队列就会从事件队列中取出信号,obj对象依附于线程B,就会调用obj中相对应的slot槽函数。所以槽函数的调用与对象的依附性有关,依附哪条线程,理解到这点就很容易理解第五个参数的连接方式。

// TestThread类
#include "testthread.h"
#include <QDebug>

TestThread::TestThread(QObject *parent) : QThread(parent){}

void TestThread::run() {
    qDebug() << "TestThread begin, tid: " << QThread::currentThreadId();
    for (int var = 0; var < 6; ++var) {
        qDebug() << "TestThread run, var: " << var;
        sleep(1);
    }
    emit testSignal();
    exec();
    qDebug() << "TestThread end...";
}
#include "testobj.h"
#include <QDebug>
#include <QThread>

TestObj::TestObj(QObject *parent) : QObject(parent){}

void TestObj::testSolt() {
    qDebug() << "TestObj slot tid: " << QThread::currentThreadId();
}

Qt::DirectConnection

直接在发送信号的线程中调用槽函数,等价于槽函数的实时调用,无论信号和槽函数是否在同一线程。

Qt::DirectConnection
// main.cpp

#include "widget.h"
#include <QApplication>
#include <QDebug>
#include <QThread>
#include "testobj.h"
#include "testthread.h"

// Qt::DirectConnection
void direct_connection() {
    static TestObj m;
    static TestThread t;
    t.start();
    t.wait(6000);
    t.quit();
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::DirectConnection);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main begin, tid: " << QThread::currentThreadId();
    direct_connection();
    qDebug() << "main end...";
    return a.exec();
}
Qt::DirectConnection

从这个结果来看,显然跟事先预期的结果不一样,我们事先说槽函数对象依附在哪个线程就在哪个线程下执行,反而TestObj的打印却在子线程TestThread里,这就是因为Qt::DirectConnection改变了依附性,使得它立即执行,谁发射的信号就在哪条线程里执行。

Qt::QueuedConnection

信号发送至目标线程的事件队列中,由目标线程处理,当前线程继续向下执行,下面的例子可以证明。

Qt::QueuedConnection
// Qt::QueuedConnection
void queued_connenction() {
    static TestObj m;
    static TestThread t;
    t.start();
    t.wait(6000);
    t.quit();
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::QueuedConnection);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main begin, tid: " << QThread::currentThreadId();
    queued_connenction();
    qDebug() << "main end...";
    return a.exec();
}
Qt::QueuedConnection

Qt::AutoConnect

该连接方式是Qt默认值,平台会根据线程依附性而自行选择DirectConnection或者QueuedConnection的连接方式,项目开发中没有特殊的情况下,一般都会默认使用该连接方式处理。

  • 如果发射信号线程 等于 目标线程,则连接方式默认为Qt::DirectConnection;
  • 如果发射信号线程 不等于 目标线程,则连接方式默认为Qt::QueuedConnection。
// QT::AutoConnection
void auto_connection() {
    static TestObj m;
    static TestThread t;
    t.start();
    t.wait(6000);
    t.quit();
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::AutoConnection);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main begin, tid: " << QThread::currentThreadId();
    auto_connection();
    qDebug() << "main end...";
    return a.exec();
}

因为m线程依附于主线程,和t线程是完全不同的线程,所以Qt会默认以Qt::QueuedConnection方式进行连接,所以结果和上面的Qt::QueuedConnection的例子相同。

Qt::BlockingQueuedConnection

信号发送至目标线程的事件队列,由目标线程处理,当前线程等待槽函数返回,然后在继续向下执行,需要注意的是当前线程和目标线程必须是不同,如果相同当前发射信号的线程永远也无法等到目标线程处理槽函数的返回

Qt::BlockingQueuedConnection
// QT::BlockingQueuedConnection
void blocking_queued_connection() {
    static TestObj m;
    static TestThread t;
    t.start();
    t.wait(6000);
    t.quit();
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::BlockingQueuedConnection);
}
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main begin, tid: " << QThread::currentThreadId();
    blocking_queued_connection();
    qDebug() << "main end...";
    return a.exec();
}
Qt::BlockingQueuedConnection

从结果来看,线程t发射信号后,主线程会执行槽函数,然后槽函数执行完成后再继续执行线程t未执行的代码,也就可以解释为什么线程t结束打印会在最后了。

Qt::UniqueConnection

默认情况下,同一个信号可以多次连接同一个槽函数,即同一个槽函数多次被调用,但是UniqueConnection连接方式只允许存在一次连接,连接方式则跟Qt::AutoConnection一样,需要根据线程依附性决定。使用场景是避免多次连接影响结果。

// QT::UniqueConnection
void unique_connection() {
    static TestObj m;
    static TestThread t;
    t.start();
    t.wait(6000);
    t.quit();
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::UniqueConnection);
    QObject::connect(&t, SIGNAL(testSignal()), &m, SLOT(testSolt()), Qt::UniqueConnection);
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug() << "main begin, tid: " << QThread::currentThreadId();
    unique_connection();
    qDebug() << "main end...";
    return a.exec();
}

当上面两个连接没有添加Qt::UniqueConnection,执行结果会发现槽函数被执行两次,而添加之后,会发现槽函数执行结果只有一次。

Qt::SingleShotConnection

5.15版本引入的,是一次性连接类型。当信号首次触发并调用槽函数后,连接会自动断开。之后的信号触发将不会再调用该槽函数。适用于那些只需要处理一次的信号,比如初始化操作、一次性事件处理或需要在第一次响应后自动取消的事件。

#include <QCoreApplication>
#include <QObject>
#include <QDebug>

class Receiver : public QObject {
    Q_OBJECT
public slots:
    void mySlot() { qDebug() << "SingleShotConnection: Slot executed"; }
};

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

    Sender sender;
    Receiver receiver;

    QObject::connect(&sender, &Sender::mySignal, &receiver, &Receiver::mySlot, Qt::SingleShotConnection);

    sender.sendSignal();  // Slot被执行
    sender.sendSignal();  // Slot不执行

    return a.exec();
}