Message Loop 原理及应用

此文已由作者王荣涛授权网易云社区发布。

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

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

结构

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

在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消息循环则可能是这样:

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

在其他系统或机制下,它还有各自独特的实现,但都大体相似。

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

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

消息产生后源不会也无法主动推给消息循环。以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让线程间通信变得足够灵活。

如上图,运行消息循环的两个线程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任务需要处理时,如果有尚未到期的递延任务,那么需要在源上开启一个定时器在递延任务到期后解除消息泵的阻塞。因此,要支持递延任务的源必须具备第三个特点,那就是支持定时唤醒。

参考资料:

http://docs.libuv.org/en/latest/loop.html

https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx.aspx)

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSRunLoop_Class/

https://docs.google.com/document/d/1_pJUHO3f3VyRSQjEhKVvUU7NzCyuTCQshZvbWeQiCXU/

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

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

相关文章:
【推荐】 中秋福利|10本技术图书(编程语言、数据分析等)免费送

原文地址:https://www.cnblogs.com/zyfd/p/9803021.html

时间: 2024-10-22 05:29:13

Message Loop 原理及应用的相关文章

Thread message loop for a thread with a hidden window? Make AllocateHwnd safe

Thread message loop for a thread with a hidden window? I have a Delphi 6 application that has a thread dedicated to communicating with a foreign application that uses SendMessage() and WM_COPYDATA messages to interface with external programs. Therefo

如何利用OpenSSL生成证书

此文已由作者赵斌授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 一.前言 最近为了测试内容分发网络(Content Delivery Network,简称 CDN)添加的新功能,支持HYTTPS安全加速功能,需要对证书的有效性进行验证,于是乎需要自己生成合法的.非法的.过期的证书.接下来介绍下如何通过OpenSSL生成证书. 二.使用OpenSSL生成证书 创建证书密钥文件 openssl genrsa -des3 -out ca.key 8192 运行时会提示输入密码

Android6.0 消息机制原理研究

 消息都是存放在一个消息队列中去,而消息循环线程就是围绕这个消息队列进入一个无限循环的,直到线程退出.如果队列中有消息,消息循环线程就会把它取出来,并分发给相应的Handler进行处理:如果队列中没有消息,消息循环线程就会进入空闲等待状态,等待下一个消息的到来.在编写Android应用程序时,当程序执行的任务比较繁重时,为了不阻塞UI主线程而导致ANR的发生,我们通常的做法的创建一个子线程来完成特定的任务.在创建子线程时,有两种选择,一种通过创建Thread对象来创建一个无消息循环的子线程:

Android消息机制Handler的实现原理解析

Android的主线程为什么可以一直存在? 线程是一个动态执行的过程,从产生到死亡包括五个状态:新建.就绪.运行.死亡和堵塞.只要线程没有执行完毕或者没有被其它线程杀死,线程就不会进入死亡状态.Android中的主线程一直存在是因为主线程中一直在监听消息,从而使线程无法被执行完毕. 线程的五种状态: 新建new Thread 当创建Thread类的一个实例对象时,此线程进入新建状态未被启动. 就绪runnable 线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等

Android 消息机制 (Handler、Message、Looper)

综合:http://blog.csdn.net/dadoneo/article/details/7667726 与 http://android.tgbus.com/Android/androidnews/201204/421642.shtml 一. 消息机制常用类的介绍和使用 在Android程序运行中,线程之间或者线程内部进行信息交互时经常会使用到消息,如果我们熟悉这些基础的东西及其内部的原理,将会使我们的Android开发变的容易.可以更好地架构系统.在学习Android消息机制之前,我们

android消息机制原理详解

android消息机制原理详解 因为之前使用的是CSDN默认的文本编辑器,而且也因为懒得学用MarkDown来写博客,所以排版上有一些问题.就上一篇写的设计模式之抽象工厂模式提出了这个问题(一个android群的群友提出来的,没有在评论里评论),所以以后的文章都用MarkDown来写了. 好了,言归正传,这篇文章我来介绍一下android消息机制的原理 Android消息机制概述 说到Android的消息机制,Android初级工程师(不包括那些初学者)肯定会想到Handler.是的,Andro

Android Message Handling Mechanism

转自:http://solarex.github.io/blog/2015/09/22/android-message-handling-mechanism/ Android is a message driven, message driven several elements: The message says: Message Message queue: MessageQueue The news cycle, remove the message processing for circ

Android 线程 Looper.prepare()、Looper.loop() 使用

优化项目过程中发现了一个很Low的问题,整理一下,备忘: 说问题之前先看下HandlerThread的定义 一个封装了looper的线程: Looper用于封装了android线程中的消息循环,默认情况下一个线程是不存在消息循环(message loop)的,需要调用Looper.prepare()来给线程创建一个消息循环,调用Looper.loop()来使消息循环起作用,从消息队列里取消息,处理消息. 注:写在Looper.loop()之后的代码不会被立即执行,当调用后mHandler.get

Android Message机制

[java] view plaincopyprint? 网上以文档形式流传,不知道原文在哪,感谢原作者了! ================简单调整了下格式就共享了=============================================== 对于Android的Message机制主要涉及到三个主要的类,分别是Handler.Message.Looper:首先对每个类做一个简单介绍:然后再介绍所谓的Android的Message机制是如何实现的,最后给了一个示例. 一.介绍三个相