分享

Qt Object模型及其线程和事件处理

 torony 2016-02-17

几个重要结论

QObject、线程和事件处理对象类图如下:
这里写图片描述
大多数Qt类的实现都采用”外部接口类+内部实现类“,即QAbc类作为接口给用户使用,但QAbc类的功能基本上在QAbcPrivate实现。
每个线程都以QThread实例表示,并且在内部拥有一个QThreadData来表示线程的基本数据。
每个线程维护了一个QPostEvent队列,用来保存待处理的事件(如鼠标、键盘以及用户事件),同时每个线程维护了一个QEventLoop栈,但只有栈顶的QEventLoop对象参与当前事件处理

Qt中重要的对象包括:QObject及其派生类对象、线程及其线程对象、事件及事件处理、signal/slot及其connection。
它们之间的关系如下:
**(1)QObject对象可以作为其他QObject的父亲,并其销毁时也会自动销毁所有的子对象。可以在父对象中获取子对象
(2)包括QThread对象在内,每个QObject对象都可以属于一个线程,但QObject与其父对象须属于同一线程,如某对象有父对象或是widget,则不能更换线程
(3)事件处理是基于线程,即如果某个QObject对象不属于线程(如何做到?),则该对象无法参与事件处理
(4)每个QObject对象都有数据结构包含了其所有signals有关的connections,而signal/slots的实现依赖于事件处理**

这里也有几个问题,可进一步思考:
问:QObject对象都是有对应的线程的,但是QThread对象并不属于它产生的线程,而是属于一开始创建QThread对象的线程。这会产生什么问题?
问:QThreadData为何会存在多个QEventLoop?

QObject

所有的Qt对象的基类是QObject,而QObject包含了其内部QObjectPrivate对象(在QObject的构造函数中初始化),QObject的需要的内部成员数据,基本上都放在QObjectPrivate里。Qt内部的类实现,往往会采用XXXClass + XXXClassPrivate机制,包括QObject及其许多派生类,都是如此。所有的XXXPrivate都派生于QObjectData,并提供了在内部QAbc和QAbcPrivate类中互相访问的快捷函数)。体可参见 Qt源码的Private Class实现介绍

QObjectPrivate及其基类QObjectData的关键成员介绍如下:

/* QObjectData */
// 当前QObject 对象的父对象
QObject *parent ;
// 当前QObject 对象的子对象  
QObjectList children;

/* QObjectPrivate */
// 用户输入的数据
ExtraData *extraData ;
// 该对象所属的线程对象   
QThreadData *threadData ;
// 该对象作为 sender的所有 connections
QObjectConnectionListVector *connectionLists ;
// 该对象作为 receiver的所有 connections
Connection *senders ;     
// object currently activating the object
Sender *currentSender ;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

从上面代码看,每个QObject都可以有parent和children,当用户在当前对象A构造函数中传入一个B作为父对象时,将当前对象A的parent成员生成B,并在B的children中加入A。当父对象销毁时,它所有的children也将被销毁。

QThreadData是该对象所属的线程对象,在QObject创建时在其构造函数中被设置。
QObjectConnectionListVector作为一个类,派生于QVector,可以看做是一个二维数组,对该对象中的每个signals(每个signal都有序号,作为vector的index),都有1个ConnectionList对应。所以这里的connectionLists,是当前对象作为connection的sender角色,所拥有的所有connections。
Connction *sender则是一个简单的connection链表,表示当他作为conection的receiver角色是,所拥有的所有connections。

Connection的定义如下:
struct Connection
{
    QObject *sender ;
    QObject *receiver ;
    union {
        StaticMetaCallFunction callFunction ;
        QtPrivate ::QSlotObjectBase * slotObj;
    };
    // The next pointer for the singly-linked ConnectionList
    Connection *nextConnectionList;
    //senders linked list
    Connection *next;
    Connection **prev;
    ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

QThread和QThreadData:线程及其描述数据

Qt中对线程的抽象是QThread类。用户创建起对象后,调用QThread::start()函数启动线程,QThread::run()是被新线程调用的内部虚函数,默认调用QThread::exec()来启动event loop,也可以派生该类后修改其内容。需要注意的是,QThread对象属于原先的线程,但QThread::run()函数是跑在新的线程中。
QThread的Private类是QThreadPrivate,里面包含成员QThreadData *data。这个QThreadData包含了这个线程的一些重要数据,比如下面的几个成员就非常重要:

QStack<QEventLoop *> eventLoops;
QPostEventList postEventList;  // 当前线程的待处理事件列表
QThread *thread; // 当前线程的线程对象
Qt:: HANDLE threadId; // 实际的线程句柄
QAtomicPointer<QAbstractEventDispatcher > eventDispatcher; // 事件分发器,负责读取和处理数据
  • 1
  • 2
  • 3
  • 4
  • 5

每个Qt线程拥有一个QThreadData实例,当QThread对象被创建时,QThreadData在其构造函数中也被创建。
这个指针也被各个QObject内部QObjectPrivate的成员threadData所引用。每次QObject对象创建时,其内部threadData指向父对象的threadData或者当前线程数据。这样,QObject能够与唯一的Thread绑定。

事件处理是建立在线程上进行的,每个线程拥有一个待处理事件列表postEventList,保存了待处理的事件QPostEvent,同时使用QEventLoop对象对其进行处理。

事件处理过程

Qt的事件处理分成两种类型,当用户将事件封装成QEvent或者其派生类对象后,会使用下面的两种API来处理:
(1)直接发送事件到接收方,接收方的时间响应函数会直接被调用,中间没有任何队列等缓存参与处理
bool QCoreApplication::sendEvent(QObject * receiver, QEvent * event)
(2)将事件加入接收方的事件队列中,等接收方所在的线程event loop来处理这些事件
void QCoreApplication::postEvent(QObject * receiver, QEvent * event, int priority = Qt::NormalEventPriority)

第一种API内部实现机制比较简单,其函数调用链如下,最终会执行到时间接收对象的虚函数event(),从而用户可以在其中加入自己的处理代码:

bool QCoreApplication:: sendEvent( QObject * receiver, QEvent * event)
--> bool QCoreApplication:: notifyInternal( QObject * receiver, QEvent * event)
-->--> bool QCoreApplication:: notify( QObject * receiver, QEvent * event)
-->-->--> bool QCoreApplicationPrivate::notify_helper(QObject *receiver, QEvent * event)
-->-->-->--> receiver-> event( event);
  • 1
  • 2
  • 3
  • 4
  • 5

第二种API涉及到的机制,则是真正的事件机制。整个过程分成两步,当事件发生时,并不直接执行处理函数,而是将其放入事件队列;第二步Event Loop则从事件队列中取出事件进行处理。
第一步如的函数调用链如下:

void QCoreApplication::postEvent( QObject * receiver, QEvent * event, int priority)
--> receiver's QThreadData: postEventList.addEvent(QPostEvent(receiver, event, priority))
  • 1
  • 2

此时事件已经在线程的事件列表中,第二步,Qt会使用QEventLoop对象来处理事件。QEventLoop抽象了事件处理过程,一般每个线程都可以有一个甚至多个QEventLoop对象。如Qt就在main()函数中调用QCoreApplication::exec()函数,该函数内部会创建一个QEventLoop对象,调用给对象的exec()将进入一个事件循环,除非接收到退出事件。

int QEventLoop::exec( ProcessEventsFlags flags)
{
    ...
     while (!d->exit.loadAcquire())
        processEvents(flags | WaitForMoreEvents | EventLoopExec);
     ...
}    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

进一步,该对象会调用QCoreApplication的事件分发

bool QEventLoop::processEvents( ProcessEventsFlags flags)
{
    Q_D(QEventLoop);
    if (!d->threadData->eventDispatcher.load())
        return false;
    return d->threadData->eventDispatcher.load()->processEvents(flags);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意上面代码d表示QEventLoop的内部对象QEventLoopPrivate,而其QThreadData *成员threadData则维护了该对象的线程情况,最后eventDispatcher的类型是QAtomicPointer。使用QAbstractEventDispatch是一个抽象类,在不同的OS和环境下,使用的实际类并不同。在Linux中如果window system使用了GTK,则需使用QEventDispatcherGlib。当调用QEventDispatcherGlib::processEvents()时,Glib会遍历一次其内部的window事件,并通过钩子函数通知用户继续处理(详细见下节)。在钩子函数中,用户会调用静态函数QCoreApplication::sendPostedEvents(),将当前线程中事件队列的所有事件,调用sendEvent()分派处理。
事件接收、分派以及处理过程如下:

QEventLoop:exec()  // 事件处理的入口
--> QEventDispatcherGlib::processEvent() // 具体操作系统的事件处理入口
-->--> QCoreApplicationPrivate:sendPostedEvents() // 静态函数,依次处理线程事件队列中的所有事件
-->-->--> QCoreApplication::sendEvent(); // 对事件立刻进行处理
  • 1
  • 2
  • 3
  • 4

看上面的流程,QEventLoop之所以从QEventDispatherGlib那里饶了一个圈又回到QCoreApplication::sendPostedEvents(),主要目的还是要从window系统拿到其它如鼠标键盘等事件。
那么问题来了,QEventDispatherGlib是如何将那些window事件放入当前线程事件列表(postEventList)呢?目标对象从哪儿得到?window system会找到当前激活窗口?

事件分发内部过程以及QAbstractEventDispatcher使用

QAbstractEventDispatcher是事件分发的抽象类,根据不同OS或环境,有不同的派生类,比如在很多Linux下window manegement使用了glib,所以有派生类QEventDispatcherGlib完成了事件分发的过程。
每个线程都有1个QEventDispatcherGlib对象,被保存在QThreadData::eventDispatcher中。

下面介绍在Qt中,Glib库是如何支持QEventDispatcherGlib接收并分发window system发过来的事件。更详细的细节请参考Glib的接口文档。

首先如上所述,QEventLoop::exec()会调用QEventDispatcherGlib::processEvent(),该函数会一直被循环执行,直至退出,而该函数内部则使用了Glib的GMainContext对象,并调用g_main_context_iteration()函数,该函数将能够遍历一次GMainContext提供的主循环一次,并有参数确定如果没有事件准备好是否需要的等待。

那么GMainContext对象包含什么内容,又是如何创建的呢?
首先在QEventDispatcherGlibPrivate构造函数中,新建GMainContext对象。
然后使用函数g_source_new()创建GSource事件源对象,这里需要传入一组回调函数指针GSourceFuncs,一般包括prepare/check/dispatch/finalize这4种回调函数,用于获知事件状态。并使用g_source_attach()将GSource对象绑定到GMainContext对象。
最后如前面所讲,g_main_context_iteration()函数会对GMainContext对象进行检查,会调用前面定义的回调函数。

在Qt中,分别为post event,socket notification,normal timer,和idle timer这4种事件,创建了4个事件源对象,并将其绑定到GMainContext对象中。

我们关心的是post event事件,Qt在其dispatch回调函数中调用了QCoreApplication::sendPostedEvents()函数,该函数会取出当前线程QThreadData::postEventList中的所有QPostedEvent,进行事件处理。
这里有一个问题:除了用户自己调用QCoreApplication::PostEvent()之外,对于鼠标键盘等系统事件,它们是何时、如何被填入QThreadData::postEventList里的?

Modal对话框的实现

Model对话框的特点就是,当函数调用modal对话框时,将一直不返回,一直等到用户关闭对话框为止。也就是说,调用modal对话框的函数将被堵塞,那么main event loop将被堵塞,这样的话,那些鼠标键盘事件将如何传给程序?
答案是modal对话框将自己创建一个QEventLoop对象,来做事件处理。这个过程在QDialog::exec()里完成。在那里,将调用QEventLoop::exec()。为了防止鼠标键盘传入其他窗口,传入的参数是QEventLoop::DialogExec。

一个线程允许多于1个QEventLoop实例来处理事件?是的,QThreadData中的成员eventLoops,就说明了这个事实,就像Modal对话框。

几个问题

当QThread对象析构时,本来运行在QThread::run()内部的对象(其父对象不是该QThread对象)会如何处理?会被删除吗?
答:QThread本身并不维护在它内部运行的QObject对象,所有的QObject对象的生命周期归其父对象或程序员自己管理。因此,当这些对象所在的线程被删除时,它们依然存活。QThread对象所对应的QThreadData对象内部有一个引用计数,起到了SHARED_PTR的作用,由于QTheadData对象作为QThread对象的一个描述,同时也被QOjbect引用,所以QThread对象被析构时,并没有销毁QThreadData对象,而仅仅将其thread设成0。另外,当调用QThread::exit()时,将在线程中退出所有的LoopEvent。
即当thread退出或者QThread对象析构时,信号所依赖的event将无法运作。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多