最近一直在倒腾事件循环的东西,通过查看Qt源码多少还是有点心得体会,在这里记录下和大家分享。总之,对于QStateMachine状态机本身来说,需要有QEventLoop::exec()的驱动才能支持,也就是说,在你Qt程序打开的时候,最后一句

QCoreApplication::exec()

已经由内部进入了状态循环

int QCoreApplication::exec()
{
...
QThreadData *threadData = self->d_func()->threadData;
if (threadData != QThreadData::current()) {
qWarning("%s::exec: Must be called from the main thread", self->metaObject()->className());
return -;
}
if (!threadData->eventLoops.isEmpty()) {
qWarning("QCoreApplication::exec: The event loop is already running");
return -;
} QEventLoop eventLoop;
self->d_func()->in_exec = true;
self->d_func()->aboutToQuitEmitted = false;
int returnCode = eventLoop.exec();
...
}

由上面我们可以得到以下几个结论:

  1. 很自然而然的我们可以看到,事件队列只跟线程有关,即同一个线程,如论你如何更改,最终你的事件循环和事件队列本身都是属于这个线程的。
  2. QApplication::exec()这种都会去最终调用QEventLoop::exec()形成事件循环。

其实不仅仅是QApplication,我们知道QDialog有类似的exec()函数,其实内部也会进入一个局部的事件循环:

int QDialog::exec()
{
...
QEventLoop eventLoop;
d->eventLoop = &eventLoop;
QPointer<QDialog> guard = this;
(void) eventLoop.exec(QEventLoop::DialogExec);
if (guard.isNull())
return QDialog::Rejected;
d->eventLoop = ;
...
}

可以看到,QDialog的这种exec()其实内部也是最终产生了一个栈上的QEventLoop来进行事件循环。这个时候,肯定有同学会有如下疑问:

  • 那如果我在QApplication::exec()中调用了QDialog的exec(),那QEventLoop如何来分配指责?

其实答案在上面已经有了,对于一个线程来说,其所拥有的事件队列是唯一的,但其所拥有的事件循环可以是多个,但绝对是嵌套关系,并且是只有当前QEventLoop被激活。我们可以看QEventLoop的exec()内部究竟在做什么。

int QEventLoop::exec(ProcessEventsFlags flags)
{
Q_D(QEventLoop);
...#if defined(QT_NO_EXCEPTIONS)
while (!d->exit)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
#else
try {
while (!d->exit)
processEvents(flags | WaitForMoreEvents | EventLoopExec);
} catch (...) {
...
}

可以看到其内部正是在通过一个while循环去不断的processEvents(),我们再来看processEvents():

bool QEventLoop::processEvents(ProcessEventsFlags flags)
{
Q_D(QEventLoop);
if (!d->threadData->eventDispatcher)
return false;
if (flags & DeferredDeletion)
QCoreApplication::sendPostedEvents(, QEvent::DeferredDelete);
return d->threadData->eventDispatcher->processEvents(flags);
}

可以很明显的看到,对于一个线程来说,无论其事件循环是内层嵌套还是在外层,其最终都会去调用

d->threadData->eventDispatcher

这个是线程唯一的,从而也证明了我们上面的结论,事件队列对于线程来说是一对一的。那么如何来验证我们另一个观点,即在同一个线程上事件循环可以是多个,并且是嵌套关系,当前只有一个激活呢?我们写一个小的Demo来验证一下:

MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
} MainWindow::~MainWindow()
{
delete ui;
} void MainWindow::on_pushButton_clicked()
{
QDialog dialog;
dialog.exec();
}

很简单,我们在MainWindow上放一个button,他的点击函数会出现一个dialog并且进入局部事件循环,之后我们在QEventLoop::exec()下断点,分别查看在没打开Dialog之前和打开之后调用栈的区别:

    QEventLoop::processEvents    qeventloop.cpp        0xb717dfc3
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QCoreApplication::exec qcoreapplication.cpp 0xb7181098
QApplication::exec qapplication.cpp 0xb74c7eaa
main main.cpp 0x804a4ce

这是没打开Dialog之前,可以看到此时的事件循环正是QCoreApplication内部提供的QEventLoop。当我们打开Dialog之后再来查看

    QEventLoop::processEvents    qeventloop.cpp        0xb717dfc3
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QDialog::exec qdialog.cpp 0xb7a949c4
...
QEventDispatcherGlib::processEvents qeventdispatcher_glib.cpp 0xb71b7cc6
QGuiEventDispatcherGlib::processEvents qguieventdispatcher_glib.cpp 0xb7595140
QEventLoop::processEvents qeventloop.cpp 0xb717e061
QEventLoop::exec qeventloop.cpp 0xb717e1cf
QCoreApplication::exec qcoreapplication.cpp 0xb7181098
QApplication::exec qapplication.cpp 0xb74c7eaa
main main.cpp 0x804a4ce

可以看到此时的事件循环正是QDialog的exec(),其实也很好理解,内部的exec()不退出,自然就不能运行外部的exec(),但千万别以为此时就事件阻塞了,很多人跟我一样,一开始总以为QDialog::exec()就会造成事件阻塞,其实事件循环依旧在不断处理,唯一的区别就是这时的事件循环是在QDialog上。

理解了基本的事件循环和事件队列之后,让我们再来看一下QStateMachine与事件循环的关联:

首先我们来看一下QStateMachine自己的postEvent()

void QStateMachine::postEvent(QEvent *event, EventPriority priority)
{
...
switch (priority) {
case NormalPriority:
d->postExternalEvent(event);
break;
case HighPriority:
d->postInternalEvent(event);
break;
}
d->processEvents(QStateMachinePrivate::QueuedProcessing);
}

可以看到,他其实内部自己维护了两个队列,一个是普通优先级的externalEventQueue,一个是高优先级的internalEventQueue。由此我们也可以得出Qt官方文档所说的状态机的事件循环和队列跟我们上文提的事件队列和事件循环压根就是两码事,千万别搞混了。可以看到他内部也会进行processEvents(),我们来看一下:

void QStateMachinePrivate::processEvents(EventProcessingMode processingMode)
{
Q_Q(QStateMachine);
if ((state != Running) || processing || processingScheduled)
return;
switch (processingMode) {
case DirectProcessing:
if (QThread::currentThread() == q->thread()) {
_q_process();
break;
} // fallthrough -- processing must be done in the machine thread
case QueuedProcessing:
processingScheduled = true;
QMetaObject::invokeMethod(q, "_q_process", Qt::QueuedConnection);
break;
}
}

很显然,状态机的实现逻辑就是把_q_process()这个异步调用,放到事件队列中去,这也印证了官方文档所说的

Note that this means that it executes asynchronously, and that it will not progress without a running event loop.

这句话,也就是说状态机的运转就是向当前线程的事件队列丢一个_q_process(),然后等待事件循环给他进行调用,所以接下来问题的关键就是_qt_process()

void QStateMachinePrivate::_q_process()
{
...
 Q_Q(QStateMachine);
Q_ASSERT(state == Running);
Q_ASSERT(!processing);
processing = true;
processingScheduled = false; while (processing) {
if (stop) {
processing = false;
break;
}
QSet<QAbstractTransition*> enabledTransitions;
QEvent *e = new QEvent(QEvent::None);
enabledTransitions = selectTransitions(e);
if (enabledTransitions.isEmpty()) {
delete e;
e = ;
}
...
enabledTransitions = selectTransitions(e);
if (enabledTransitions.isEmpty()) {
delete e;
e = ;
}
}
if (!enabledTransitions.isEmpty()) {
q->beginMicrostep(e);
microstep(e, enabledTransitions.toList());
q->endMicrostep(e);
}#endif
if (stop) {
stop = false;
stopProcessingReason = Stopped;
...
}

可以看到,状态机的process本身就是一个大循环,flag为processing(这也是避免多次投递_q_process()的标记位),进入此函数后状态机会去根据状态迁移表去调用相应的函数。这里面其实也有可以扩展的地方,就是当我的状态机本身去调用的函数是一个不返回的,也就是说比如QDialog::exec(),进入了事件循环,那我此时的状态机会卡在

microstep(e, enabledTransitions.toList());

这个函数上,我们也知道exec()函数可以让我们正常进行事件派发,所以当事件队列又去调用状态机事件的时候,因为上文processing这个flag的存在,我们在

void QStateMachinePrivate::processEvents(EventProcessingMode processingMode)
{
Q_Q(QStateMachine);
if ((state != Running) || processing || processingScheduled)
return;
...
}

会立即返回,所以你也不需要去担心状态机的阻塞以及效率问题,因为此时他只做队列的post维护,但processEvents()压根不能执行。

这个问题还有一个有意思的地方是需要注意的,就拿我们之前的语境,状态机本身调用的函数会去调用一个QDialog::exec(),那么在创建好dialog之后,我的事件循环就在这个dialog中的QEventLoop开始做了,所以有一点需要注意就是我的_q_process()

void QStateMachinePrivate::_q_process()
{
Q_Q(QStateMachine);
Q_ASSERT(state == Running);
Q_ASSERT(!processing);
processing = true;
processingScheduled = false;
#ifdef QSTATEMACHINE_DEBUG
qDebug() << q << ": starting the event processing loop";
#endif
while (processing) {
if (stop) {
processing = false;
break;
}
...
}

因为while循环的存在,所以我的队列可能此时有3个事件,A,B,C,其中我执行A的时候我创建了个Dialog,此时我的所有事件循环都建立在这个新创建的dialog的内部的那个QEventLoop,那么当我关闭这个Dialog的时候,我while继续执行,但此时我所在的事件循环已经是QCoreApplication的exec内部的QEventLoop了,这点需要特别注意。

还有一个需要注意的是倘若你想让状态机在执行耗时函数的时候可以立即返回或者像上文一样出现Dialog,此时状态机不能继续循环,但你的需要是想让状态机可以继续正常运行处理别的事件的时候,你就需要在状态机处理事件的内部调用

bool QMetaObject::invokeMethod();

这个函数,通过第三个参数选择Qt::QueuedConnection你可以很轻松的把这个dialog投递当QEventLoop的事件队列中,而让当前状态机正常返回,然后QEventLoop的processEvents()会去处理这个dialog,并创建之后调用exec()形成局部事件循环。

总体来说,需要记住以下几点:

  • 事件队列对于线程来说是一对一的,而事件循环对于线程来说是多对一的,但他们是嵌套关系,并且只有当前QEventLoop被激活。
  • 状态机的驱动需要通过现存的事件循环来推动,并且其内部维护的事件队列和QEventLoop的事件队列是两回事。
  • 当状态机的_q_process()没有返回的时候,Qt不会再去派发_q_process事件。并且总会在_q_process循环中针对当前的所有状态机事件进行逐步处理。

深入理解QStateMachine与QEventLoop事件循环的联系与区别的更多相关文章

  1. 深入理解javascript中的事件循环event-loop

    前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...

  2. 理解 node.js 的事件循环

    node.js 的第一个基本观点是,I/O 操作是昂贵的: 目前的编程技术最大的浪费来自等待 I/O 操作的完成.有几种方法可以解决这些对性能的影响(来自Sam Rushing): 同步:依次处理单个 ...

  3. Qt事件循环与状态机事件循环的思考

    写下这个给自己备忘,关于事件循环以及多线程方面的东西我还需要多多学习.首先我们都知道程序有一个主线程,在GUI程序中这个主线程也叫GUI线程,图形和绘图相关的函数都是由主线程来提供.主线程有个事件循环 ...

  4. JS:事件循环机制、调用栈以及任务队列

    点击查看原文 写在前面 js里的事件循环机制十分有趣.从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的. 在之前,我只是简单地认为由于函数执行很快,setTimeout执行 ...

  5. QT中的线程与事件循环理解(1)

    1.需要使用多线程管理的例子 一个进程可以有一个或更多线程同时运行.线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理.Qt 使用QThread 来管 ...

  6. QT虚拟小键盘设计--qt事件循环,事件发送的理解

    有人讲到QT5.7及其以后的版本才自带免费的小键盘插件. QT5.10中关于QKeyEvent类:点击打开链接 QT sendEvent和PostEvent, 点击打开链接 my god,我今天安装了 ...

  7. JavaScript:彻底理解同步、异步和事件循环(Event Loop) (转)

    原文出处:https://segmentfault.com/a/1190000004322358 一. 单线程 我们常说"JavaScript是单线程的". 所谓单线程,是指在JS ...

  8. 关于Qt的事件循环以及QEventLoop的简单使用

    1.一般我们的事件循环都是由exec()来开启的,例如下面的例子: 1 QCoreApplicaton::exec() 2 QApplication::exec() 3 QDialog::exec() ...

  9. 深入理解 JavaScript 事件循环(一)— event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

随机推荐

  1. Poj 2528-Mayor's posters 线段切割

      题目:http://poj.org/problem?id=2528 Mayor's posters Time Limit: 1000MS   Memory Limit: 65536K Total ...

  2. Shell如何传递字符串

    Shell 在写函数的时候,有时候需要传递字符串,由于字符串中有空格,所以结果总是不对,下面写个小例子,解决这个问题: #!/bin/bash # value init TT="adb sh ...

  3. 在终端中创建一个简单的mysql表格

    打开终端后输入:/usr/local/MySQL/bin/mysql -u root –p 然后输入密码:***** 创建数据库:create database work; 使用当前数据库:use w ...

  4. 一个分门别列介绍JavaScript各种常用工具的脑图

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:一个分门别列介绍JavaScript各种常用工具的脑图.

  5. iOS OC开发代码规范

    1.变量.类名.函数名 使用驼峰命名法 2.尽量使用完整的单词命名,尽量不采用 缩写单词 3.类名使用大写字母打头,前缀统一加上HH 例如:HHHomePageController 4.类的成员变量使 ...

  6. iOS中二维码的生成与使用(入门篇)

    这里简单总结一下关于二维码的扫描与生成,用的是原生的AVFoundation框架,其实这个框架目前功能还是够用的,不过这里推荐一个二维码扫描的第三方(face++),网址就不贴了,直接度娘就OK,里面 ...

  7. Quartz定时任务学习(七)Cron 触发器

    Cron表达式 Quartz使用类似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示: 位置 时间域名 允许值 允许的特殊字符 1 秒 0-59 ...

  8. java.sql.SQLException: Before start of result set解决方法

    java.sql.SQLException: Before start of result set解决方法 今天做东西的时候发现这个错误,查了查,特地记下来,以后开始积累了 哈哈 解决发法是: 使用r ...

  9. CSharp - Comparison between IComparer and IComparable

    /* Author: Jiangong SUN */ I've already written an article introducing the usage of comparer here. I ...

  10. iOS swift使用xib绘制UIView

    目标:用xib绘制一个UIView,在某个ViewController中调用. 三个文件:ViewController.Swift    DemoView.swift     DemoView.xib ...