14.QueuedConnection和BlockingQueuedConnection连接方式源码分析

QT信号槽直连时的时序和信号槽的连接方式已经在前面的文章中分析过了,见https://blog.csdn.net/Master_Cui/article/details/109011425https://blog.csdn.net/Master_Cui/article/details/109228521,本文对QueuedConnection和BlockingQueuedConnection连接方式进行源码分析

当信号槽的连接方式是QueuedConnection

if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread) || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv);
                continue;
#if QT_CONFIG(thread)
}

上述代码表明:当信号槽的连接方式是QueuedConnection时,直接调用的是queued_activate,然后 continue 进行下次循环;

看下先queued_activate的实现

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv)
{
    const int *argumentTypes = c->argumentTypes.loadRelaxed();//获取connection中的信号函数的参数类型
    if (!argumentTypes) {//如果信号函数的参数类型为空,重新获取
        QMetaMethod m = QMetaObjectPrivate::signal(sender->metaObject(), signal);//根据发送信号的元对象和和信号索引获取信号函数
        argumentTypes = queuedConnectionTypes(m.parameterTypes());//将信号函数的参数封装在一个数组中并返回数组的首地址
        if (!argumentTypes) // cannot queue arguments//异常检测
            argumentTypes = &DIRECT_CONNECTION_ONLY;
        if (!c->argumentTypes.testAndSetOrdered(0, argumentTypes)) {
            if (argumentTypes != &DIRECT_CONNECTION_ONLY)
                delete [] argumentTypes;
            argumentTypes = c->argumentTypes.loadRelaxed();
        }
    }
    if (argumentTypes == &DIRECT_CONNECTION_ONLY) // cannot activate//异常检测
        return;
    int nargs = 1; // include return type
    while (argumentTypes[nargs-1])//获取信号参数数组中参数的个数
        ++nargs;

    QBasicMutexLocker locker(signalSlotLock(c->receiver.loadRelaxed()));
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected before we got the lock
        return;
    }
    if (c->isSlotObject)
        c->slotObj->ref();
    locker.unlock();

    QMetaCallEvent *ev = c->isSlotObject ?//如果connection中包含一个有用槽函数指针的对象,那么,对应QT5的新语法
    //如果connection中仅仅是一个函数指针callFunction,对应QT4语法,这样就能让QT同时支持两种语法
        new QMetaCallEvent(c->slotObj, sender, signal, nargs) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs);

    void **args = ev->args();
    int *types = ev->types();

    types[0] = 0; // return type
    args[0] = nullptr; // return value

    if (nargs > 1) {
        for (int n = 1; n < nargs; ++n)
            types[n] = argumentTypes[n-1];//将argumentTypes中的参数类型填充到QMetaCallEvent中

        for (int n = 1; n < nargs; ++n)
            args[n] = QMetaType::create(types[n], argv[n]);//利用参数类型和参数的序号找到参数对应的值,然后填充到QMetaCallEvent中
    }
    //填充到QMetaCallEvent中的参数类型和参数值主要是因为:信号参数的指针数组在栈上。一旦信号退出,这些指针数组将不再有效。

    locker.relock();
    if (c->isSlotObject)
        c->slotObj->destroyIfLastRef();
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected while we were unlocked
        locker.unlock();
        delete ev;
        return;
    }

    QCoreApplication::postEvent(c->receiver.loadRelaxed(), ev);//将拥有参数类型和参数值的QMetaCallEvent放到事件队列中
}

所以,queued_activate的基本思路就是将信号的参数信息放到QMetaCallEvent中,然后放到循环事件队列中等待发送

QMetaCallEvent的定义如下

class Q_CORE_EXPORT QMetaCallEvent : public QAbstractMetaCallEvent
{
public:
    // blocking queued with semaphore - args always owned by caller
    QMetaCallEvent(ushort method_offset, ushort method_relative,
                   QObjectPrivate::StaticMetaCallFunction callFunction,
                   const QObject *sender, int signalId,
                   void **args, QSemaphore *semaphore);
    QMetaCallEvent(QtPrivate::QSlotObjectBase *slotObj,
                   const QObject *sender, int signalId,
                   void **args, QSemaphore *semaphore);

    // queued - args allocated by event, copied by caller
    QMetaCallEvent(ushort method_offset, ushort method_relative,
                   QObjectPrivate::StaticMetaCallFunction callFunction,
                   const QObject *sender, int signalId,
                   int nargs);
    QMetaCallEvent(QtPrivate::QSlotObjectBase *slotObj,
                   const QObject *sender, int signalId,
                   int nargs);
                   
    //四个构造函数分别对象QT4和QT5的语法,并且分别对应QueuedConnection和BlockingQueuedConnection的连接方式
    ~QMetaCallEvent() override;

    inline int id() const { return d.method_offset_ + d.method_relative_; }
    inline const void * const* args() const { return d.args_; }//该函数返回一个const指针,该const指针指向一个const void的指针
    inline void ** args() { return d.args_; }
    inline const int *types() const { return reinterpret_cast<int*>(d.args_ + d.nargs_); }
    inline int *types() { return reinterpret_cast<int*>(d.args_ + d.nargs_); }

    virtual void placeMetaCall(QObject *object) override;

private:
    inline void allocArgs();

    struct Data {
        QtPrivate::QSlotObjectBase *slotObj_;//成员槽函数指针的对象的指针
        void **args_;//指向存储参数指针的数组的二级指针
        QObjectPrivate::StaticMetaCallFunction callFunction_;//函数指针,就是MOC文件中的qt_static_metacall
        int nargs_;//参数的个数
        ushort method_offset_;//信号方法的偏移
        ushort method_relative_;//信号方法的相对序号
    } d;
    // preallocate enough space for three arguments
    char prealloc_[3*(sizeof(void*) + sizeof(int))];
};

QCoreApplication::postEvent的实现如下

void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority)
{
    Q_TRACE_SCOPE(QCoreApplication_postEvent, receiver, event, event->type());

    if (receiver == 0) {//异常处理
        qWarning("QCoreApplication::postEvent: Unexpected null receiver");
        delete event;
        return;
    }

    QThreadData * volatile * pdata = &receiver->d_func()->threadData;
    QThreadData *data = *pdata;//获取接收者所在的线程数据
    if (!data) {
        // posting during destruction? just delete the event to prevent a leak
        delete event;
        return;
    }

    // lock the post event mutex
    data->postEventList.mutex.lock();
    //事件将发送到每个线程的事件队列(QThreadData::postEventList)中。 
    //排队的事件受互斥锁保护,因此当线程将事件推送到另一个线程的事件队列时,没有竞争条件。

    // if object has moved to another thread, follow it//接收者有可能被移动到其他线程中运行,那么将线程数据的指针重新指向
    while (data != *pdata) {
        data->postEventList.mutex.unlock();

        data = *pdata;
        if (!data) {
            // posting during destruction? just delete the event to prevent a leak
            delete event;
            return;
        }

        data->postEventList.mutex.lock();
    }

    QMutexUnlocker locker(&data->postEventList.mutex);

    // if this is one of the compressible events, do compression
    if (receiver->d_func()->postedEvents
        && self && self->compressEvent(event, receiver, &data->postEventList)) {
        Q_TRACE(QCoreApplication_postEvent_event_compressed, receiver, event);
        return;
    }
    //compressEvent作用是判断一个事件是否已经被删除,如果被删除,则不应该添加到事件队列中

    if (event->type() == QEvent::DeferredDelete)//关于DeferredDelete事件的处理,暂时不用管
        receiver->d_ptr->deleteLaterCalled = true;

    if (event->type() == QEvent::DeferredDelete && data == QThreadData::current()) {//关于DeferredDelete事件的处理,暂时不用管
        // remember the current running eventloop for DeferredDelete
        // events posted in the receiver's thread.

        // Events sent by non-Qt event handlers (such as glib) may not
        // have the scopeLevel set correctly. The scope level makes sure that
        // code like this:
        //     foo->deleteLater();
        //     qApp->processEvents(); // without passing QEvent::DeferredDelete
        // will not cause "foo" to be deleted before returning to the event loop.

        // If the scope level is 0 while loopLevel != 0, we are called from a
        // non-conformant code path, and our best guess is that the scope level
        // should be 1. (Loop level 0 is special: it means that no event loops
        // are running.)
        int loopLevel = data->loopLevel;
        int scopeLevel = data->scopeLevel;
        if (scopeLevel == 0 && loopLevel != 0)
            scopeLevel = 1;
        static_cast<QDeferredDeleteEvent *>(event)->level = loopLevel + scopeLevel;
    }

    // delete the event on exceptions to protect against memory leaks till the event is
    // properly owned in the postEventList
    QScopedPointer<QEvent> eventDeleter(event);//在处理事件的线程中对其进行处理后,将立即删除该事件
    Q_TRACE(QCoreApplication_postEvent_event_posted, receiver, event, event->type());
    data->postEventList.addEvent(QPostEvent(receiver, event, priority));//将事件放入循环队列中
    eventDeleter.take();
    event->posted = true;
    ++receiver->d_func()->postedEvents;
    data->canWait = false;
    locker.unlock();

    QAbstractEventDispatcher* dispatcher = data->eventDispatcher.loadAcquire();//取得eventDispatcher,Linux下对应的是QEventDispatcherGlib的指针
    if (dispatcher)
        dispatcher->wakeUp();//唤醒事件循环迭代上下文mainContext
}

void QEventDispatcherGlib::wakeUp()
{
    Q_D(QEventDispatcherGlib);
    d->postEventSource->serialNumber.ref();
    g_main_context_wakeup(d->mainContext);
}

g_main_context_wakeup会唤醒事件循环,处理事件队列中的事件,关于事件的分析见https://blog.csdn.net/master_cui/category_10463698.html

所以,postEvent实际上是将事件放到对应线程的事件循环中,所以当我们使用QueuedConnection对信号槽进行连接时,会先执行信号函数所在线程的代码,槽函数此时在对应线程的循环队列中等待,等信号函数所在线程的循环队列没有事件时,再执行槽函数所在线程的代码

通过事件循环机制,最终,事件会被事件处理函数接受被处理,而遇到QEvent::MetaCall时,以bool QObject::event(QEvent *e)为例,该类型的事件处理方式如下

bool QObject::event(QEvent *e)
{
    //..........
    case QEvent::MetaCall:
    {
        QAbstractMetaCallEvent *mce = static_cast<QAbstractMetaCallEvent*>(e);

        if (!d_func()->connections.loadRelaxed()) {
            QBasicMutexLocker locker(signalSlotLock(this));
            d_func()->ensureConnectionData();
        }
        QObjectPrivate::Sender sender(this, const_cast<QObject*>(mce->sender()), mce->signalId());

        mce->placeMetaCall(this);
        break;
    }
    //..........
}

上述代码的核心就是那句mce->placeMetaCall(this);

而placeMetaCall是个纯虚函数,在子类QMetaCallEvent中实现,代码如下

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_);
    }
}

可见,当connect是对应QT5的语法时,调用的是d.slotObj_->call(object, d.args_);,直接进行函数调用,而当connect是对应QT4的语法时,直接调用函数指针,而函数指针指向的是MOC文件中的qt_static_metacall,具体见博客https://blog.csdn.net/Master_Cui/article/details/109011153https://blog.csdn.net/Master_Cui/article/details/109011218

当信号槽的连接方式是BlockingQueuedConnection时的代码如下

 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);
                }
                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
}

而当信号槽的连接方式是BlockingQueuedConnection时,直接通过事件循环机制将MetaCall事件放入循环队列中发送。只不过用信号量保护起来

上述代码的信号量的资源数为0,当信号槽都处于同一个线程时,信号先调用,执行到BlockingQueuedConnection分支时,QCoreApplication::postEvent(receiver, ev);调用结束后立刻返回,接着会执行semaphore.acquire();,acquire的默认实参是1,所以,此时线程会阻塞,又因为槽函数也处在当前线程(的事件队列)中,所以,槽函数也没法执行,而该信号量还没有release操作,所以,当信号槽处于同一线程且连接方式是BlockingQueuedConnection,会造成死锁。

而当信号和槽函数处于两个不同的线程时,虽然semaphore.acquire()会导致信号所在的线程阻塞,但不会导致槽函数所在的线程阻塞,当QMetaCallEvent被事件处理程序处理后,会立刻被删除,释放内存,而QMetaCallEvent的父类是QAbstractMetaCallEvent(是个纯虚基类),QAbstractMetaCallEvent的析构函数如下

QAbstractMetaCallEvent::~QAbstractMetaCallEvent()
{
#if QT_CONFIG(thread)
    if (semaphore_)
        semaphore_->release();
#endif
}

利用C++的多态机制,当QMetaCallEvent在槽函数的线程被销毁时,会调用QAbstractMetaCallEvent的析构函数,在析构函数中调用semaphore_->release();将信号量的资源释放,从而使得信号函数所在的线程的阻塞状态解除

所以,当信号槽的连接方式是BlockingQueuedConnection,如果信号槽处于同一线程,那么因为信号量的原因,会造成程序的死锁,但是如果信号槽处于不同线程,那么当槽函数所在的线程将会将信号量的资源释放掉,所以信号线程会先因为信号量阻塞,等待槽函数执行结束后将信号量的资源释放,接着信号线程获得信号量,然后继续执行

关于信号量见官方文档:https://doc.qt.io/qt-5/qsemaphore.html#details

官方文档中有个比喻特别好:信号量就像在餐厅用餐。 餐厅中的椅子数量就是信号量的资源。 当客人到餐厅吃饭时,会占座位。 然后椅子资源会递减。当没有椅子时,再来的人们就需要等待有椅子空出来。 随着人们的离开,椅子数会增加,从而允许更多的人进入餐厅吃饭。 但是,如果有10个人去吃饭,但只有9个座位,则这10人将一直等待,除非信号量添加新的资源(餐厅加新的椅子)。如果来了4个人吃饭,那么此时可用座位只有5个,那10个人的等待时间会更长。

 

参考

https://doc.qt.io/qt-5/qsemaphore.html#details

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

 

欢迎大家评论交流,作者水平有限,如有错误,欢迎指出

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值