个性化阅读
专注于IT技术分析

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.get();
    UIText.update(data);
}

假设这是可以的,那么使用10个这样的线程对UI进行更新,那么UI更新的是哪个数据呢?这就是线程安全的问题了,允许子线程更新UI会造成线程安全的问题,除非你使用线程同步的操作,但是现在GUI结构并不是对UI加锁,而是使用线程通信。

那么更新UI的标准方式为:开启一个子线程A请求任务,完成后将请求结果通过线程通信发送给UI主线程main,UI主线程获取到数据后对UI进行更新。

Event Loop运行循环/事件循环

Event Loop运行循环-事件循环

现在回到Event Loop,整体看来,一个GUI程序的整体运行逻辑如上图所示,左边是UI主线程,右边是工作线程,线程间的通信参考不同环境下的线程通信API,又称为信息传递。

这里解释一下Event(事件),这个Event指的是已经处理后的结果,例如点击事件,Event的获取并不是指点击的整个过程,而是该事件造成的结果。那么一个Event就是对一个事件的静态描述,例如发生的时间、所在UI元素、坐标位置等等。

通常Event并不是仅仅指UI触摸事件,其它的还有Timer定时器、线程间通信或监控当前loop的事件,这是对事件源的一种分类Category。不同的GUI环境可能有不同的定义,首先要明白这其中的原理,具体定义也不会相差多少的。

从事件的发起源,以上就是对事件源的分类

另一个角度是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,其扩展的内容是创建一个常驻后台线程、线程间通信,后面的继续讨论相关的内容。

赞(0)
未经允许不得转载:srcmini » GUI编程基本原理之:event loop和run loop(运行循环)

评论 抢沙发

评论前必须登录!