GUI编程基本原理之(event loop和run loop(运行循环))

event loop是什么?为什么不能在子线程更新UI?你可能听过Android中的looper、iOS中的事件循环、JavaScript的Event Loop等等,这些都是类似的概念。GUI编程中可能最容易犯错或忽略的问题是:在子线程中更新UI,本文将一一为你解答这些问题,实际也是为了在不同环境的GUI编程更加顺利。
在计算机编程中,event loop(运行循环或事件循环)是一个编程结构或设计模式,它用于等待或分发事件或消息。event loop的主要工作是发送请求到event provider进行处理,当有事件或消息返回时,event loop进行分发(处理)。event loop在不同的环境中可能有不同的称呼:message dispatcher、message loop、message pump或run loop。
当event loop是程序的主结构的时候,又称为main loop或main event loop。
首先下面先从最简单的情况说起——
自定义GUI编程我们一般的GUI编程所涉及的API都是系统提供的,例如iOS中的UIKit。在我们的一般编程中,最简单的情况就是命令行编程,就是在main函数中进行操作。假如让我们自己编写GUI程序,那该怎么写呢?下面是一个简单的模拟例子:

int main(int argc, char **argv){ initApp(); // APP启动初始化 UIDataType *uidata; // loop while (1){ // 获取事件 eventTask = EventQueue.receive(); // 处理事件,并适当更新UI数据 int exit = process(uidata, eventTask); drawUI(uidata); // 重绘UI if(exit == 1) break; // 结束程序 } return 0; }

首先要说明,UI算法所处理的主要是围绕UI数据结构,然后使用一个无限循环(loop),首先是从事件队列中获取一个事件任务,并进行处理,可适当对UI数据进行更新,接着调用UI绘制。
那么在我们实际的GUI编程是怎么样的呢?首先使用的组件都是系统提供的,例如Android中的Activity和iOS中的Controller,这些东西都是在这个loop中间的。否则的话一运行程序所有代码,程序就会结束,但是我们的GUI程序并不会立即退出的,所以是使用了一个loop。
由上面你可以看到,所有东西都是在主线程中执行的(当然应用启动可能还会启动其它线程,但是我们首先的编程的地方还是主线程),再说,什么是线程?一个线程单独占有自己的栈空间,多个线程各自占有各自的栈空间。
那么问题来了,我们在哪里进行UI更新呢?在Android或iOS中,你可以直接在Activity-oncreate和iOS的ViewDilLoad中更新UI是没有问题的——这是在主线程中执行UI更新,当然没有问题。
这个操作可以在drawUI后进行自定义操作:
int main(int argc, char **argv){ initApp(); // APP启动初始化 UIDataType *uidata; // loop while (1){ // 获取事件 eventTask = EventQueue.receive(); // 处理事件,并适当更新UI数据 int exit = process(uidata, eventTask); drawUI(uidata); // 重绘UI if(exit == 1) break; // 结束程序 customUpdateUI(uidata); } return 0; }

这样在下一次循环时,UI就会被重绘,也就是UI被更新了。
一般GUI中的主要结构就是类似上面给出的例子,这个while循环就称为loop,也就是event loop事件循环,首先要说明这个事件队列还是在主线程中的,队列中是一些回调任务,主要是对UI的响应操作。
因为正常情况下,一个无限循环是不会退出的,那么很显然在一个线程中只有一个这样的loop。
如何更新UI?正常情况下,我们可以在主线程中进行UI更新。但是问题来了,如果项目的算法操作和UI更新操作都放在主线程,那么整个程序将会变得异常慢!因而一些效率不是超好的任务不要放在主线程执行,而在一些环境的GUI编程中会禁止你这样做。
这就是说,UI更新操作放在主线程执行,但是算法操作尽量放在其它主线程执行。
接着的问题是:我们可能会开一个线程请求网络数据,然后将请求的数据更新到UI,我们可能会写以下代码:
httpThread.request{ data = http://www.srcmini.com/http.get(); UIText.update(data); }

假设这是可以的,那么使用10个这样的线程对UI进行更新,那么UI更新的是哪个数据呢?这就是线程安全的问题了,允许子线程更新UI会造成线程安全的问题,除非你使用线程同步的操作,但是现在GUI结构并不是对UI加锁,而是使用线程通信。
那么更新UI的标准方式为:开启一个子线程A请求任务,完成后将请求结果通过线程通信发送给UI主线程main,UI主线程获取到数据后对UI进行更新。
Event Loop运行循环/事件循环
GUI编程基本原理之(event loop和run loop(运行循环))

文章图片
现在回到Event Loop,整体看来,一个GUI程序的整体运行逻辑如上图所示,左边是UI主线程,右边是工作线程,线程间的通信参考不同环境下的线程通信API,又称为信息传递。
这里解释一下Event(事件),这个Event指的是已经处理后的结果,例如点击事件,Event的获取并不是指点击的整个过程,而是该事件造成的结果。那么一个Event就是对一个事件的静态描述,例如发生的时间、所在UI元素、坐标位置等等。
通常Event并不是仅仅指UI触摸事件,其它的还有Timer定时器、线程间通信或监控当前loop的事件,这是对事件源的一种分类Category。不同的GUI环境可能有不同的定义,首先要明白这其中的原理,具体定义也不会相差多少的。
【GUI编程基本原理之(event loop和run loop(运行循环))】从事件的发起源,以上就是对事件源的分类。
另一个角度是Loop,也就是在一次Loop中只处理一种事件,比如只处理UI触摸事件,或者只处理其它线程发来的事件。这个不一定每个环境都有,但是要注意,如果某些GUI编程有更复杂的定义要分析清楚,否则可能会造成很多编程错误。
另外,在GUI编程对应的系统可能会提供定义事件任务的推荐形式,例如定义一个worker,其中提供子线程任务请求,并提供主线程更新UI的形式。这是将子线程请求任务和UI线程处理事件共同封装在一起了。
小结本文主要讨论的是GUI编程中的event loop运行循环或事件循环:
  • 一个线程对应于一个main loop,只能在UI线程上对UI进行更新,以保证操作UI数据线程安全。
  • 耗时任务如网络操作或其它算法操作,建议放在其他线程进行处理,通过线程通信或其它方式发送消息给UI线程进行更新。
  • UI线程处理事件,可根据事件源进行分类,或者在一次loop中处理不同类型的事件源。
  • 通常只是在主线程中有一个run loop,但是在子线程也可以创建run loop,这可以创建一个常驻后台线程。
本文仅限于讨论GUI的event loop,其扩展的内容是创建一个常驻后台线程、线程间通信,后面的继续讨论相关的内容。

    推荐阅读