Message Loop 原理及应用


此文已由做者王荣涛受权网易云社区发布。html

欢迎访问网易云社区,了解更多网易技术产品运营经验。nginx


Message loop,即消息循环,在不一样系统或者机制下叫法也不尽相同,有被叫作event loop,也有被叫作run loop或者其余名字的,它是一种等待和分派消息的编程结构,是经典的消息驱动机制的基础。为了方便起见,本文对各系统下相似的结构统称为message loop。django

结构

Message loop,顾名思义,首先它是一种循环,这和咱们初学C语言时接触的for、while是同一种结构。编程

在Windows下它多是这个样子的:windows

MSG msg;BOOL bRet;
...while (bRet = ::GetMessage(&msg, NULL, 0, 0)) {    if (bRet == -1) {        // Handle Error
    } else {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
}复制代码

在iOS下它多是这个样子的:安全

BOOL shouldQuit = NO;
...BOOL ok = YES;
NSRunLoop *loop = [NSRunLoop currentRunLoop];while (ok && !shouldQuit) {
    ok = [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}复制代码

而用libuv实现的I/O消息循环则多是这样:bash

bool should_quit = false;
...
uv_loop_t *loop = ...while (!should_quit) {
    uv_run(loop, UV_RUN_ONCE);
}复制代码

在其余系统或机制下,它还有各自独特的实现,但都大致类似。网络

事实上,正常运行过程当中在接到特殊消息或者指令以前,它就是一个完全的死循环!同时,这样的结构也决定了它更多意义上是一种单线程上的设计。也正由于如此,对这种编程结构进行了封装的系统(好比iOS)也每每不保证或者根本不屑于说起其线程安全性。而多线程共享的消息循环在笔者看来在绝大部分场景下都属于逆天的设计,本文只讨论单线程上的消息循环。多线程

Loop前面有个定语message,进一步代表它要处理的对象,即消息。这里说的消息是广义上的消息,它多是UI消息、通知、I/O事件等等。那么消息从哪里来?消息循环又从哪里提取它们?这在不一样系统或机制下有所不一样:有来自消息队列的,有来自输入源/定时器源的,有来自异步网络、文件完成操做通知的,还有来自可观察对象状态变化的等等。这里把消息循环提取消息的源统称为消息源,简称源。app

消息产生后源不会也没法主动推给消息循环。以Windows消息为例,一条异步窗口消息产生后它会被存放在窗口所属线程的消息队列上,若是消息循环不采起任何措施,那么它将永远没法被处理。消息循环从消息队列中去抽取,它才能被取出并分派。这种从消息队列中抽取消息的机制,咱们叫作消息泵。

生命期

Message loop的生命期始于线程执行过程当中第一次进入该循环的循环体,终于循环被break或者线程被强行终止那一刻,而二者之间即是运行期。

运行期内,消息泵不停尝试从源那里抽取消息,若是源内消息非空,那么消息将被当即取出,接着被分派处理。若是源内没有消息,消息循环便进入空载(idling)阶段。就像水池中没有水时抽水泵开着是浪费电能同样,若是消息泵在空载时也无休止地工做也将浪费几乎全部的CPU资源。为了解决这个问题,须要消息泵在空载时可以自我阻塞,这种特征每每须要源来提供。源的另外一个特色是在新消息到达以后将阻塞中的消息泵(准确说是消息循环所在线程)唤醒,使之恢复工做。以上面的例子来讲,GetMessage、NSRunLoop.runMode:beforeDate:以及uv_run操做的对象都具有这两个特色。

新消息的添加可能来自于本线程也可能来自于其余线程,甚至包括其余进程中的线程。另外不少系统提供了对待处理消息的撤销或者移除操做,好比Windows下的PeekMessage、CancelIo分别能够移除待处理的UI消息和I/O操做,iOS下的NSRunLoop.cancelPerformSelectorsWithTarget:族方法则能够撤销待处理的selector。

结束消息循环的过程和结束一个普通的for、while循环大体相同,就是改变循环控制表达式的值使之不知足继续循环的条件。不一样的地方在于,普通循环每每是自发的,而消息循环可能来自外部的需求,而后经过某种方式通知该消息循环让其自我退出。另外一种结束消息循环的方式是强制停止其所属线程的执行,固然了,这是极不推荐的。

嵌套

Message loop是能够嵌套(nested)的,简而言之就是Loop1上在处理一个任务的过程当中又起了一个另外一个Loop2。请看如下场景:

void RunLoop() {    while (GetMessage(&msg)) {
        ...
        ProcessMessage(&msg);
        ...
    }
}void Start() {
    RunLoop(); // 进入Loop1}void ProcessMessage(MSG *msg) {
    ...    if (msg->should_do_foo_bar) {
        Foo();
        RunLoop(); // 进入Loop2,嵌套!
        Bar();
    }
    ...
}复制代码

嵌套的一个典型案例就是模态对话框。在模态对话框返回以前此后的语句不会被执行,好比上例中Bar在RunLoop返回以前不会被执行,由于Loop1在Loop2启动后就处于阻塞状态了,这就引出了嵌套消息循环的一个特色:任什么时候刻有且只有一个Loop是活动的,其他都是被阻塞的。嵌套消息循环的另外一个特色是它们同属于一个线程,反过来讲,非同线程的message loop没法造成嵌套。

嵌套的一个比较明显的坑:若是Bar运行须要资源R,而R在Loop2生命期内被释放了,那么等Loop2生命期结束后Loop1恢复执行,第一个调用的就是Bar,此时R已经不存在了,Bar的代码若是缺少足够的保护就有可能会引发crash!

多线程通讯

Message loop让线程间通讯变得足够灵活。

Alt pic

如上图,运行消息循环的两个线程Thread 1和Thread 2之间经过向对方的消息队列中投递消息来进行通讯,这个过程是彻底异步的。

结合前文提到的消息循环嵌套技术,多线程通讯时,通讯发起线程能够在不阻塞本线程消息处理的前提下等待对方回应后再进行后续操做。以上文中的Foo和Bar为例,若是Foo异步请求资源,Bar处理接收到的资源,Loop 2等到资源被接收后当即结束,那么它们三者宏观上看起来像是一次同步资源请求和处理操做,并且在此期间Thread 1和Thread 2消息处理顺畅!这很是奇妙,在不少状况下比阻塞式的傻等有用多了。

然而,消息投递过程自己是跨线程的操做,对于使用C++这样的Native语言开发的场景,这意味着朴素地操道别的线程的消息队列自己就存在隐患,因此通常须要对消息队列进行锁保护。此外,线程间通常推荐只持有对方消息队列的弱引用,不然很容易陷入循环引用或者致使野指针范围——试想若是Thread 2先退出,其消息队列实体也被销毁,此后若是Thread 1尝试经过Thread 2消息队列的裸指针向其投递消息势必形成灾难。

多线程之间通讯比较难以处理的是消息的撤销和资源的管理,可是这个不在本文的讨论范围以内,若是有时间,笔者将在将来撰文讨论这个问题。

附加机制

至此,本文描述的消息循环仅仅在处理消息自己,其实咱们在消息循环中还能够加入一些十分有用的机制,这里介绍其中最经常使用的两种。

空闲任务(Idle tasks)是在消息循环处于空载状态时被处理的任务。消息循环空载每每意味着没有特别紧要的消息须要处理,这个时候是处理空闲任务的绝佳时机,好比发送一些后台统计数据。以基于libuv的I/O消息循环为例,对其稍加改动即可加入这种机制:

class UVMessageLoop {public:
    ...private:    bool should_quit_;    bool message_processed_;
    uv_loop_t *loop_;
};void UVMessageLoop::OnUVNotification(uv_poll_t *req, int status, int events) {
    UVMessageLoop *loop = static_cast<UVMessageLoop *>(req->data);
    ...
    loop->message_processed_ = true;
}void UVMessageLoop::Run() {    for (;;) {
        uv_run(loop, UV_RUN_ONCE);        if (should_quit_)            break;        if (message_process_) {            // 刚刚处理了一条消息
            continue;
        }        // 没有消息,处理idle task
        bool has_idle_task = DoIdleTasks();        if (should_quit_)            break;        if (has_idle_task) {            continue;
        }        // idle task都没有,再抽取一次消息,没有就自我阻塞
        uv_run(loop, UV_RUN_NOWAIT);
    }
}复制代码

注意上例中两次uv_run调用的第二个参数是不一样的,UV_RUN_NOWAIT用于尝试从源抽取并处理一次I/O事件可是若没有也当即返回;而UV_RUN_ONCE则是在没有事件的时候被阻塞直到新事件到达。须要注意的是,在uv_run处理事件的时候最终会同步调用到UVMessageLoop::OnUVNotification,这样其返回后能够经过检查message_processed_来知道是否有消息被处理了。

递延任务(Deferred tasks)是晚于投递时间被执行的任务,好比在播放动画时使用它能够在帧时间到达时才真正渲染某个帧。继续以基于libuv的I/O消息循环为例,做以下改动后能够加入这种机制:

class UVMessageLoop {public:
    ...private:    bool should_quit_;    bool message_processed_;
    TimeTicks deferred_task_time_;
    uv_loop_t *loop_;
    uv_timer_t *timer_;
};void UVMessageLoop::OnUVNotification(uv_poll_t *req, int status, int events) {
    UVMessageLoop *loop = static_cast<UVMessageLoop *>(req->data);
    ...
    loop->message_processed_ = true;
}void UVMessageLoop::OnUVTimer(uv_timer_t* handle, int status) {
    ...
}void UVMessageLoop::Run() {    for (;;) {
        uv_run(loop, UV_RUN_ONCE);        if (should_quit_)            break;        if (message_process_) {            // 刚刚处理了一条消息
            continue;
        }        // 没有消息,处理递延任务,同时获取下一个递延任务的时间
        bool has_deferred_task = DoDeferredTasks(&deferred_task_time_);        if (should_quit_)            break;        if (has_deferred_task) {            continue;
        }        // 也没有递延任务,处理idle task
        bool has_idle_task = DoIdleTasks();        if (should_quit_)            break;        if (has_idle_task) {            continue;
        }        // 没有idle task
        if (delayed_task_time_.is_null()) {            // 也没有deferred task,再抽取一次消息,没有就自我阻塞
            uv_run(loop_, UV_RUN_ONCE);
        } else {
            TimeDelta delay = delayed_task_time_ - TimeTicks::Now();            if (delay > TimeDelta()) {                // 设置定时器,若是在定时器到期前尚未其余事件到达而被解除阻塞,
                // 那么uv_run将由于定时到期事件而被解除阻塞
                uv_timer_start(timer_, OnUVTimer, delay.ToMilliseconds(), 0);
                uv_run(loop_, UV_RUN_ONCE);
                uv_timer_stop(timer_);
            } else {                // 有递延任务未及时处理,进入下一轮后处理
                delayed_task_time_ = TimeTicks();
            }
        }        if (should_quit_)            break;
    }
}复制代码

因为递延任务通常优先级高于空闲任务,因此咱们先于空闲任务处理它们。另外deferred_task_time_记录了下一个递延任务的单调递增时间(好比当前线程的clock值),当没有I/O事件须要处理且也没有Idle任务须要处理时,若是有还没有到期的递延任务,那么须要在源上开启一个定时器在递延任务到期后解除消息泵的阻塞。所以,要支持递延任务的源必须具有第三个特色,那就是支持定时唤醒。

参考资料:

docs.libuv.org/en/latest/l…

msdn.microsoft.com/en-us/libra….aspx)

developer.apple.com/library/mac…

docs.google.com/document/d/…



网易云免费体验馆,0成本体验20+款云产品!

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 客户端SDK测试思路
【推荐】 收集、分析线上日志数据实战——ELK
【推荐】 django项目在uwsgi+nginx上部署遇到的坑

相关文章
相关标签/搜索