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上部署遇到的坑

相關文章
相關標籤/搜索