首页 > 其他分享 >深入探索Qt框架系列之信号槽原理(三)

深入探索Qt框架系列之信号槽原理(三)

时间:2024-05-31 09:03:21浏览次数:26  
标签:loadRelaxed Qt 探索 QObject 框架 线程 信号 receiver sender

前面两篇分别介绍了QObject::connectQMetaObject::Connection,那么信号槽机制的基础已经介绍完了,本文将介绍信号槽机制是如何从信号到槽的,以及多线程下是如何工作的。

信号槽机制源码解析

1. 信号的触发

以该系列的第一篇文章中的示例举例:
test_moc.h:

class test_moc : public QObject {
    Q_OBJECT
public:
    test_moc(QObject* parent = nullptr)
        : QObject(parent)
    {
    }
    QString name { "123" };

    using INTTYPE = int;
public slots:
    void on_TestSlot() { qDebug() << __FUNCTION__; }
    void on_TestSlot_Param(int num) { qDebug() << __FUNCTION__ << num; }
    void on_TestSlot_Param(QString num) { qDebug() << __FUNCTION__ << num; }

signals:
    void sigTestSignals();
    void sigTestSignals_Param(INTTYPE num);
};

main.cpp:

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

    test_moc m1, m2;

    QObject::connect(&m2, &test_moc::sigTestSignals_Param, &m1, [=](int num) {
        qDebug() << __FUNCTION__ << num;
    });

    emit m2.sigTestSignals_Param(1);

    return a.exec();
}

信号触发是通过emit宏实现的,在《深入探索Qt框架系列之元对象编译器》一文中已经介绍了,emit是一个空宏,并且在经过元对象编译器处理生成的代码中包含了信号的实现:

// SIGNAL 0
void test_moc::sigTestSignals()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

// SIGNAL 1
void test_moc::sigTestSignals_Param(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

所以信号的触发就是调用了QMetaObject::activate函数。

2. 信号函数底层实现原理

QMetaObject::activate函数源码就是调用了QMetaObject::doActivate,下面我们通过源码来解析:

不想看源码的可以直接看下面的小结内容。

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)
{
    QObjectPrivate *sp = QObjectPrivate::get(sender);
    
    // ... ... 省略非关键代码

    bool senderDeleted = false;
    {
    Q_ASSERT(sp->connections.loadAcquire());
    QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed());
    QObjectPrivate::SignalVector *signalVector = connections->signalVector.loadRelaxed();

    // 获取信号索引对应ConnectionList
    const QObjectPrivate::ConnectionList *list;
    if (signal_index < signalVector->count())
        list = &signalVector->at(signal_index);
    else
        list = &signalVector->at(-1);

    // 这是发送信号的线程ID(也就是调用emit所在线程的ID)
    Qt::HANDLE currentThreadId = QThread::currentThreadId();
    // 判断发送信号的线程ID和发送者对象所在线程ID是否一致
    //   两者是有可能不一致的,比如将发送者对象通过moveToThread()方法移动到另外的线程
    bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData.loadRelaxed()->threadId.loadRelaxed();

    // We need to check against the highest connection id to ensure that signals added
    // during the signal emission are not emitted in this emission.
    // 为了确保在信号发送期间新增的信号不会在当前的发送过程中被发送,我们需要检查最高的连接ID
    uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
    do {
        QObjectPrivate::Connection *c = list->first.loadRelaxed();
        if (!c)
            continue;

        do {
            QObject * const receiver = c->receiver.loadRelaxed();
            if (!receiver)
                continue;

            QThreadData *td = c->receiverThreadData.loadRelaxed();
            if (!td)
                continue;

            // 判断是否跨线程
            bool receiverInSameThread;
            if (inSenderThread) {
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            } else {
                // need to lock before reading the threadId, because moveToThread() could interfere
                QMutexLocker lock(signalSlotLock(receiver));
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            }


            // determine if this connection should be sent immediately or
            // put into the event queue
            // 判断此连接是直接调用还是放入事件队列
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                // 通过postEvent实现跨线程通信
                queued_activate(sender, signal_index, c, argv);
                continue;
#if QT_CONFIG(thread)
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                if (receiverInSameThread) {
                    // 在同一个线程下不能使用BlockingQueuedConnection连接
                    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);
                }
                // 定义局部信号量,用来同步发送者和接收者
                QSemaphore semaphore;
                {
                    QBasicMutexLocker locker(signalSlotLock(sender));
                    if (!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
            }
            // ... ... 省略代码的功能:
            // 根据槽类型做不同的处理:槽对象调用、函数指针调用、元调用(metacall)...
        } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);
        // 上面是内层循环,处理当前信号索引下的所有连接对象
    } while (list != &signalVector->at(-1) &&
        //start over for all signals;
        ((list = &signalVector->at(-1)), true));
        // 上面是外层循环,强行再做一次循环,目的是检查所有连接对象。

        if (connections->currentConnectionId.loadRelaxed() == 0)
            senderDeleted = true;
    }
    // ... ...
}

2.1. 小结

函数内部处理的整个流程如下:
信号发送处理流程 (1).png

多线程下的信号槽

1. 如何判断信号槽是否跨线程?

在正常情况下,我们一般使用AutoConnection进行连接,在明确跨线程时会用QueuedConnectionBlockingQueuedConnection。这里我们只讨论在用AutoConnection时,Qt底层是如何判断的。

从上面一节的源码可以看到,在触发信号函数时,底层涉及到三个线程,分别是:

  • 发送信号线程
  • 发送者所在线程
  • 接收者所在线程

这三个线程是要区分开来的,底层实现的逻辑是发送信号线程和接收者所在线程进行比较,如果不一致,则代表跨线程。

发送信号线程不一定是发送者所在线程,发送者所在线程可以通过moveToThread()转移。

接收者所在线程的数据存储在哪里?
《深入探索Qt框架系列之信号槽原理(二)》一文中介绍了QMetaObject::Connection结构中有维护接收者对象所在线程的数据。

2. 跨线程通信是如何实现的?

还是通过上面的源码可以知道,跨线程通信就是通过QCoreApplication::postEvent()函数实现的。
该函数将事件数据对象(QEvent的派生实例)和接收事件的QObject发送到接收事件的QObject所在的线程事件队列中,事件队列依次处理,处理到对应事件时就是调用槽函数的时候。
本文不再展开介绍postEvent()是如何工作的了,因为这里涉及到线程的事件队列机制,后面我们再详细介绍。

标签:loadRelaxed,Qt,探索,QObject,框架,线程,信号,receiver,sender
From: https://blog.csdn.net/LeoLei8060/article/details/139328928

相关文章

  • 探索计算机的外围设备
    目录介绍概述磁盘存储设备1.硬盘驱动器(HDD)2.软盘驱动器3.磁盘阵列(RAID)4.总结磁带存储设备1.卡带驱动器2.磁带机3.磁带存储设备的应用光盘和磁光盘存储设备1.光盘(CD)2.数码多功能碟(DVD)3.磁光盘(MO)显示设备1.阴极射线管(CRT)显示器2.液晶显示器(LCD)3.......
  • Qt使用qBreakpad定位崩溃位置(2)
    软件调试Qt使用qBreakpad定位崩溃位置(2)目录软件调试Qt使用qBreakpad定位崩溃位置(2)前言1、Google-Breakpad2、qBreakpad3、crashpad4、注意Linux下1、环境2、qBreakpad源码准备3、qBreakpad编译4、测试qBreakpad5、dump文件调试5.1编译breakpad5.2开始分析dmp文件Windows下1......
  • QT_5.2_matlab组合多维数组
    完整代码示例以下是一个完整的代码示例,展示如何将多个测试数据拼接成一个四维数组:%示例测试数据nn=10;%假设有10个测试样本TestData=cell(nn,6);%创建一个包含10个样本的单元格数组,每个样本包含6个二维矩阵%生成一些随机数据作为示例fork=1:nnforj=1......
  • Windows下Qt使用dump定位崩溃位置(1)
    软件调试Windows下Qt使用dump定位崩溃位置(1)目录软件调试Windows下Qt使用dump定位崩溃位置(1)1、Qt崩溃定位方法2、什么是dump文件3、使用vs调试dmp4、下载Windows符号表5、下载Qt符号表6、主要代码7、源代码更多精彩内容......
  • Qt-qrencode开发-生成、显示二维码
    Qt-qrencode开发-生成二维码......
  • QtQuick实现图片查看器
    QtQuick实现图片查看器介绍图片查看器是非常值得新手入门QtQuick的项目,通过该项目,用户可以很快熟悉QML语法和资源文件存储,还可以使用通过操作文件夹实现多图像查看。实战首先打开QtCreator,创建QtQuick项目,我使用的是Qt6.5版本,但是其他版本应该也可以。我们先新建一个QML文件,......
  • java集合框架
    java集合框架前言:本节我们来学习java集合框架1.0什么是集合集合便是对象的容器,定义了多个对象进行操作的常用方法集合和数组的区别:1数组长度固定,集合长度不固定2数组可以存储基本类型和引用类型,集合只能存储引用类型。我们在使用集合时,需要导入java.utill的所有内容1.1......
  • DeerOJ的前端框架介绍-model文件夹
    model文件夹model文件夹下存储的是一些相关类的php文件,在HTML文件生成的时候,利用这些类能够高效地辅助文件与文件之间的调度转换。文件夹下的内容如下:这里列举一些重要的类文件:Route.php文件前文中在实现route.php的路由调度过程中有出现使用类Route的情况,实际上就是调用这......
  • DeerOJ的前端框架介绍-libs文件夹和controller文件夹
    libs文件夹在index.php文件初始化的过程中,需要提前准备好一些类和方法,这些类和方法的初始化是调用libs文件夹下的大多数php文件来实现的。libs文件夹下的文件如下:其中,uoj-lib.php文件是进行所有相关初始化的主文件,上级的index.php文件会直接调用该文件展开初始化的工......
  • DeerOJ的前端框架介绍-Web文件夹和App文件夹(route.php)
    Web文件夹下的结构DeerOJ的前端框架参考了部分Lavarel框架,做到兼顾代码的可维护性和可阅读性。具体的维护目录文件结构如下:注意到web文件夹下的index.php这是整个前端程序的main程序,当服务段收到请求后,根据.htaccess文件指定使用index.php文件来生成网页,并把网页数......