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

时间:2022-02-16 09:12:27

写下这个给自己备忘,关于事件循环以及多线程方面的东西我还需要多多学习。首先我们都知道程序有一个主线程,在GUI程序中这个主线程也叫GUI线程,图形和绘图相关的函数都是由主线程来提供。主线程有个事件循环Event Loop,其实就是一个死循环在不断的等待你的消息队列,通过消息队列完成响应用户操作,绘图,以及相关操作。我们都知道QDialog有一个exec函数,这个函数会形成“模态”对话框,然后等待用户去输入OK还是Cancel,否则他绝不返回,如下

void test()
{
QDialog dialog;
dialog.exec(); qDebug() << "finish exec";
}

我们可以看这个简单的例子,可以看到,当dialog被exec之后,我们的qDebug是不会输出的,除非我们人为去点了对话框的OK,否则,就会一直卡在exec之上。这个时候可能同学会有我一开始一样的误解,我们会误以为此时事件循环停止了,其实并不是停止,而是阻塞住了。为什么会阻塞?因为test这个函数没有返回嘛!

m_stateManager->postEvent(ev);

投递事件官方API也说的很清楚,会立即返回,所以别去担心此时投递的事件进入不到消息队列,真正要关心的是此时dialog的exec让你的主线程阻塞了,这个时候消息队列上的事件都不会进行操作,都在等待dialog的返回,只有dialog返回,接下来的事件才会依次进行。要记住,消息是可以正常投递的

那么,有没有办法可以让dialog.exec()立即返回,同时我的对话框还在呢?方法是有的

void test()
{
metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection); qDebug() << "finish exec";
} Q_INVOKABLE void invokeTest()
{
QDialog dialog;
dialog.exec();
}

答案就是用Qt提供的元对象系统Meta Object System的invokeMethod,并且将第三个参数设为Qt:QueuedConnection。从字面上我们也可以看的出来,这个调用不会去立即调用,相反他是异步的,他会把这个函数投递到事件队列里,也就是说,这个例子中的qDebug会输出,输出之后,事件队列才会去调用dialog.exec()这个函数,当然了,调用这个函数之后又会达到我们一开始的那个阻塞的效果,你通过异步到最后的触发,始终你需要去面对exec给你当前事件循环造成阻塞的问题。

让我们再深入一点,当一个事件循环的时候还算简单。但是我们知道,Qt中对于状态机他是有一个异步的事件循环的,也就是说外面有事件循环,状态机本身也有事件循环。比如

m_stateManager->postEvent(ev);

m_tool->run();

你给状态机投递了一个事件,他根据状态迁移去调用你的tool,这一切看起来很美好,但如果你此时的tool是个跟之前一样的阻塞的exec呢?让我们来看一下。

void Tool::run()
{
QDialog dialog;
dialog.exec();
}

对于这个情况,当我们运行tool之后,我们的状态机就跟之前的主事件事件循环一样被阻塞了,也就是说如果我此时继续

m_stateManager->postEvent(ev2);

和之前一样,这个postEvent会立即返回,因为投递到事件队列都是立即返回的。但是关键的问题在于你的状态机整个事件循环都停止不动了,都在等你之前的tool运行结束,但因为你之前的tool是个dialog.exec()你必须手动去点OK,不然你的状态机事件循环就阻塞不动了,这个时候如果你的客户不断的去点你这个tool的event,那会产生噩梦般的效果----你点完OK之后又会来OK之后又会来OK。。。这其实就是你一旦点了OK,你的消息队列就又可以循环了,之前等待的ev就都会去执行了。而且要注意的就是,此时你的exec的执行在主线程上,只是不能进行返回,但还是可以接收诸如键盘,鼠标等事件投递。

前面也说了,事件循环和状态机循环是两个独立的循环,其实这也很好理解。如果没有事件循环,状态机事件怎么知道你有没有按下这个键?从而去投递给状态机呢?其实也就是说当你状态机事件阻塞的时候,你的主事件循环还在不断的接收你的键盘和鼠标的操作,这一点是没有影响的。

因此,要想实现在tool的时候我还能相应别的状态事件,其实做法也是一样的,就是

void Tool::run()
{
metaObject()->invokeMethod(this, "invokeTest", Qt::QueuedConnection);
} Q_INVOKABLE void invokeTest()
{
QDialog dialog;
dialog.exec();
}

立即返回,这个”立即返回“并不是说你的事情做完了,而是你更想让状态机能够进行之后事件的循环,别去因为你的dialog而耽误了大家。

最后说说模态这个主题,其实模态的理解就是你的消息队列都在正常进行,因为你不断的在等待,导致事件循环不能进行下去,必须你这边正常返回,你接下来的操作才能继续。


今天又重新思考了一下这个问题。同步的意思似乎就是必须要执行完成才能返回。异步的概念就是立即返回,之后执行,会把他扔到消息队列里,待同步函数处理完之后,然后去搜索事件队列进行操作。其实状态机归根结底就是一堆信号链接,只是他的方向是规矩状态迁移表来进行。作为主线程的Event Loop来说,当dialog进入exec的时候,就是就是在进行事件队列,并不是说他此时把事件队列给阻塞了,这个我之前理解有问题。exec的含义就是去处理事件队列,去处理事件循环,去检索当前还有哪些事件可以被处理,从而去正常处理。比如我们有一个主程序窗口MainWindow,有一个Dialog,此时我们去调用dialog的exec,内部会去创建一个QEventLoop,又因为这个dialog的所在线程和MainWindow在同一个线程上,所以看上去似乎是两个EventLoop,但实际上都是同一个线程的Event Loop(一个线程只能有一个Event Loop,这是原子性问题)。所以在dialog进行exec的时候他会去检索主线程上的事件队列去操作。

而我们之前讲的状态机,其实仔细想了想很简单,你就把他理解成是主程序总的Event Loop中的一个事件,他在进行操作的时候,不返回(tool去调用dialog的exec看上去似乎进行了事件循环在等待你新的event,但别忘了,你本身这个tool的run就是通过事件队列去触发的)所以必须要等待这个tool的exec返回,你的事件队列才能正常下去。

再次强调:

  1. exec并不是说事件队列被你阻塞,而是才是让你进入一个真正等待处理事件队列的过程。
  2. 同一个线程只能有一个Event Loop,这个可以参考CP单核心单线程的处理逻辑。
  3. 在进行事件队列进行事件操作的时候,其实内部就是同步的方式在进行,必须等待函数全部执行完毕才能真正返回才能真正进行之后的event,这也可以说的通我们之前举的状态机的例子,看上去这个状态机引发了我们的tool,tool中调用了dialog的exec,看上去似乎很美好,在等待状态事件了。但此时你这个exec不返回,你如何让事件Event Loop继续进行下去。
  4. 同步函数就是必须要等待函数操作完成之后才能返回的函数。异步函数就是直接返回,他具体什么时候进行操作,待具体实现查看。(也可能是本线程的事件队列,也可能是别的线程进行run)。如果是别的线程进行run的时候,你可能会去想这个立即返回的问题,其实很简单,Qt的run都是start,其实就跟postEvent一样,只是简单的把他注册给线程管理器,由线程管理器再去跑他的run函数,那你本地的start当然立即返回了。