摘要:此文已由作者王榮濤授權(quán)網(wǎng)易云社區(qū)發(fā)布。新消息的添加可能來(lái)自于本線程也可能來(lái)自于其他線程,甚至包括其他進(jìn)程中的線程。另一種結(jié)束消息循環(huán)的方式是強(qiáng)制中止其所屬線程的執(zhí)行,當(dāng)然了,這是極不推薦的。
此文已由作者王榮濤授權(quán)網(wǎng)易云社區(qū)發(fā)布。
歡迎訪問(wèn)網(wǎng)易云社區(qū),了解更多網(wǎng)易技術(shù)產(chǎn)品運(yùn)營(yíng)經(jīng)驗(yàn)。
Message loop,即消息循環(huán),在不同系統(tǒng)或者機(jī)制下叫法也不盡相同,有被叫做event loop,也有被叫做run loop或者其他名字的,它是一種等待和分派消息的編程結(jié)構(gòu),是經(jīng)典的消息驅(qū)動(dòng)機(jī)制的基礎(chǔ)。為了方便起見,本文對(duì)各系統(tǒng)下類似的結(jié)構(gòu)統(tǒng)稱為message loop。
結(jié)構(gòu)
Message loop,顧名思義,首先它是一種循環(huán),這和我們初學(xué)C語(yǔ)言時(shí)接觸的for、while是同一種結(jié)構(gòu)。
在Windows下它可能是這個(gè)樣子的:
MSG msg;BOOL bRet; ...while (bRet = ::GetMessage(&msg, NULL, 0, 0)) { if (bRet == -1) { // Handle Error } else { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } }
在iOS下它可能是這個(gè)樣子的:
BOOL shouldQuit = NO; ...BOOL ok = YES; NSRunLoop *loop = [NSRunLoop currentRunLoop];while (ok && !shouldQuit) { ok = [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }
而用libuv實(shí)現(xiàn)的I/O消息循環(huán)則可能是這樣:
bool should_quit = false; ... uv_loop_t *loop = ...while (!should_quit) { uv_run(loop, UV_RUN_ONCE); }
在其他系統(tǒng)或機(jī)制下,它還有各自獨(dú)特的實(shí)現(xiàn),但都大體相似。
事實(shí)上,正常運(yùn)行過(guò)程中在接到特殊消息或者指令之前,它就是一個(gè)徹底的死循環(huán)!同時(shí),這樣的結(jié)構(gòu)也決定了它更多意義上是一種單線程上的設(shè)計(jì)。也正因?yàn)槿绱耍瑢?duì)這種編程結(jié)構(gòu)進(jìn)行了封裝的系統(tǒng)(比如iOS)也往往不保證或者根本不屑于提及其線程安全性。而多線程共享的消息循環(huán)在筆者看來(lái)在絕大部分場(chǎng)景下都屬于逆天的設(shè)計(jì),本文只討論單線程上的消息循環(huán)。
Loop前面有個(gè)定語(yǔ)message,進(jìn)一步表明它要處理的對(duì)象,即消息。這里說(shuō)的消息是廣義上的消息,它可能是UI消息、通知、I/O事件等等。那么消息從哪里來(lái)?消息循環(huán)又從哪里提取它們?這在不同系統(tǒng)或機(jī)制下有所不同:有來(lái)自消息隊(duì)列的,有來(lái)自輸入源/定時(shí)器源的,有來(lái)自異步網(wǎng)絡(luò)、文件完成操作通知的,還有來(lái)自可觀察對(duì)象狀態(tài)變化的等等。這里把消息循環(huán)提取消息的源統(tǒng)稱為消息源,簡(jiǎn)稱源。
消息產(chǎn)生后源不會(huì)也無(wú)法主動(dòng)推給消息循環(huán)。以Windows消息為例,一條異步窗口消息產(chǎn)生后它會(huì)被存放在窗口所屬線程的消息隊(duì)列上,如果消息循環(huán)不采取任何措施,那么它將永遠(yuǎn)無(wú)法被處理。消息循環(huán)從消息隊(duì)列中去抽取,它才能被取出并分派。這種從消息隊(duì)列中抽取消息的機(jī)制,我們叫做消息泵。
生命期
Message loop的生命期始于線程執(zhí)行過(guò)程中第一次進(jìn)入該循環(huán)的循環(huán)體,終于循環(huán)被break或者線程被強(qiáng)行終止那一刻,而兩者之間便是運(yùn)行期。
運(yùn)行期內(nèi),消息泵不停嘗試從源那里抽取消息,如果源內(nèi)消息非空,那么消息將被立即取出,接著被分派處理。如果源內(nèi)沒(méi)有消息,消息循環(huán)便進(jìn)入空載(idling)階段。就像水池中沒(méi)有水時(shí)抽水泵開著是浪費(fèi)電能一樣,如果消息泵在空載時(shí)也無(wú)休止地工作也將浪費(fèi)幾乎所有的CPU資源。為了解決這個(gè)問(wèn)題,需要消息泵在空載時(shí)能夠自我阻塞,這種特征往往需要源來(lái)提供。源的另一個(gè)特點(diǎn)是在新消息到達(dá)之后將阻塞中的消息泵(準(zhǔn)確說(shuō)是消息循環(huán)所在線程)喚醒,使之恢復(fù)工作。以上面的例子來(lái)說(shuō),GetMessage、NSRunLoop.runMode:beforeDate:以及uv_run操作的對(duì)象都具備這兩個(gè)特點(diǎn)。
新消息的添加可能來(lái)自于本線程也可能來(lái)自于其他線程,甚至包括其他進(jìn)程中的線程。另外很多系統(tǒng)提供了對(duì)待處理消息的撤銷或者移除操作,比如Windows下的PeekMessage、CancelIo分別可以移除待處理的UI消息和I/O操作,iOS下的NSRunLoop.cancelPerformSelectorsWithTarget:族方法則可以撤銷待處理的selector。
結(jié)束消息循環(huán)的過(guò)程和結(jié)束一個(gè)普通的for、while循環(huán)大致相同,就是改變循環(huán)控制表達(dá)式的值使之不滿足繼續(xù)循環(huán)的條件。不同的地方在于,普通循環(huán)往往是自發(fā)的,而消息循環(huán)可能來(lái)自外部的需求,然后通過(guò)某種方式通知該消息循環(huán)讓其自我退出。另一種結(jié)束消息循環(huán)的方式是強(qiáng)制中止其所屬線程的執(zhí)行,當(dāng)然了,這是極不推薦的。
嵌套
Message loop是可以嵌套(nested)的,簡(jiǎn)而言之就是Loop1上在處理一個(gè)任務(wù)的過(guò)程中又起了一個(gè)另一個(gè)Loop2。請(qǐng)看以下場(chǎng)景:
void RunLoop() { while (GetMessage(&msg)) { ... ProcessMessage(&msg); ... } }void Start() { RunLoop(); // 進(jìn)入Loop1}void ProcessMessage(MSG *msg) { ... if (msg->should_do_foo_bar) { Foo(); RunLoop(); // 進(jìn)入Loop2,嵌套! Bar(); } ... }
嵌套的一個(gè)典型案例就是模態(tài)對(duì)話框。在模態(tài)對(duì)話框返回之前此后的語(yǔ)句不會(huì)被執(zhí)行,比如上例中Bar在RunLoop返回之前不會(huì)被執(zhí)行,因?yàn)長(zhǎng)oop1在Loop2啟動(dòng)后就處于阻塞狀態(tài)了,這就引出了嵌套消息循環(huán)的一個(gè)特點(diǎn):任何時(shí)刻有且只有一個(gè)Loop是活動(dòng)的,其余都是被阻塞的。嵌套消息循環(huán)的另一個(gè)特點(diǎn)是它們同屬于一個(gè)線程,反過(guò)來(lái)說(shuō),非同線程的message loop無(wú)法形成嵌套。
嵌套的一個(gè)比較明顯的坑:如果Bar運(yùn)行需要資源R,而R在Loop2生命期內(nèi)被釋放了,那么等Loop2生命期結(jié)束后Loop1恢復(fù)執(zhí)行,第一個(gè)調(diào)用的就是Bar,此時(shí)R已經(jīng)不存在了,Bar的代碼如果缺乏足夠的保護(hù)就有可能會(huì)引起crash!
多線程通信
Message loop讓線程間通信變得足夠靈活。
Alt pic
如上圖,運(yùn)行消息循環(huán)的兩個(gè)線程Thread 1和Thread 2之間通過(guò)向?qū)Ψ降南㈥?duì)列中投遞消息來(lái)進(jìn)行通信,這個(gè)過(guò)程是完全異步的。
結(jié)合前文提到的消息循環(huán)嵌套技術(shù),多線程通信時(shí),通信發(fā)起線程可以在不阻塞本線程消息處理的前提下等待對(duì)方回應(yīng)后再進(jìn)行后續(xù)操作。以上文中的Foo和Bar為例,如果Foo異步請(qǐng)求資源,Bar處理接收到的資源,Loop 2等到資源被接收后立即結(jié)束,那么它們?nèi)吆暧^上看起來(lái)像是一次同步資源請(qǐng)求和處理操作,而且在此期間Thread 1和Thread 2消息處理順暢!這非常奇妙,在很多情況下比阻塞式的傻等有用多了。
然而,消息投遞過(guò)程本身是跨線程的操作,對(duì)于使用C++這樣的Native語(yǔ)言開發(fā)的場(chǎng)景,這意味著樸素地操作別的線程的消息隊(duì)列本身就存在隱患,所以一般需要對(duì)消息隊(duì)列進(jìn)行鎖保護(hù)。此外,線程間一般推薦只持有對(duì)方消息隊(duì)列的弱引用,否則很容易陷入循環(huán)引用或者導(dǎo)致野指針?lè)秶囅肴绻鸗hread 2先退出,其消息隊(duì)列實(shí)體也被銷毀,此后如果Thread 1嘗試通過(guò)Thread 2消息隊(duì)列的裸指針向其投遞消息勢(shì)必造成災(zāi)難。
多線程之間通信比較難以處理的是消息的撤銷和資源的管理,但是這個(gè)不在本文的討論范圍之內(nèi),如果有時(shí)間,筆者將在未來(lái)撰文討論這個(gè)問(wèn)題。
附加機(jī)制
至此,本文描述的消息循環(huán)僅僅在處理消息本身,其實(shí)我們?cè)谙⒀h(huán)中還可以加入一些十分有用的機(jī)制,這里介紹其中最常用的兩種。
空閑任務(wù)(Idle tasks)是在消息循環(huán)處于空載狀態(tài)時(shí)被處理的任務(wù)。消息循環(huán)空載往往意味著沒(méi)有特別緊要的消息需要處理,這個(gè)時(shí)候是處理空閑任務(wù)的絕佳時(shí)機(jī),比如發(fā)送一些后臺(tái)統(tǒng)計(jì)數(shù)據(jù)。以基于libuv的I/O消息循環(huán)為例,對(duì)其稍加改動(dòng)便可加入這種機(jī)制:
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(req->data); ... loop->message_processed_ = true; }void UVMessageLoop::Run() { for (;;) { uv_run(loop, UV_RUN_ONCE); if (should_quit_) break; if (message_process_) { // 剛剛處理了一條消息 continue; } // 沒(méi)有消息,處理idle task bool has_idle_task = DoIdleTasks(); if (should_quit_) break; if (has_idle_task) { continue; } // idle task都沒(méi)有,再抽取一次消息,沒(méi)有就自我阻塞 uv_run(loop, UV_RUN_NOWAIT); } }
注意上例中兩次uv_run調(diào)用的第二個(gè)參數(shù)是不同的,UV_RUN_NOWAIT用于嘗試從源抽取并處理一次I/O事件但是若沒(méi)有也立即返回;而UV_RUN_ONCE則是在沒(méi)有事件的時(shí)候被阻塞直到新事件到達(dá)。需要注意的是,在uv_run處理事件的時(shí)候最終會(huì)同步調(diào)用到UVMessageLoop::OnUVNotification,這樣其返回后可以通過(guò)檢查message_processed_來(lái)知道是否有消息被處理了。
遞延任務(wù)(Deferred tasks)是晚于投遞時(shí)間被執(zhí)行的任務(wù),比如在播放動(dòng)畫時(shí)使用它可以在幀時(shí)間到達(dá)時(shí)才真正渲染某個(gè)幀。繼續(xù)以基于libuv的I/O消息循環(huán)為例,作如下改動(dòng)后可以加入這種機(jī)制:
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(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; } // 沒(méi)有消息,處理遞延任務(wù),同時(shí)獲取下一個(gè)遞延任務(wù)的時(shí)間 bool has_deferred_task = DoDeferredTasks(&deferred_task_time_); if (should_quit_) break; if (has_deferred_task) { continue; } // 也沒(méi)有遞延任務(wù),處理idle task bool has_idle_task = DoIdleTasks(); if (should_quit_) break; if (has_idle_task) { continue; } // 沒(méi)有idle task if (delayed_task_time_.is_null()) { // 也沒(méi)有deferred task,再抽取一次消息,沒(méi)有就自我阻塞 uv_run(loop_, UV_RUN_ONCE); } else { TimeDelta delay = delayed_task_time_ - TimeTicks::Now(); if (delay > TimeDelta()) { // 設(shè)置定時(shí)器,如果在定時(shí)器到期前還沒(méi)有其他事件到達(dá)而被解除阻塞, // 那么uv_run將因?yàn)槎〞r(shí)到期事件而被解除阻塞 uv_timer_start(timer_, OnUVTimer, delay.ToMilliseconds(), 0); uv_run(loop_, UV_RUN_ONCE); uv_timer_stop(timer_); } else { // 有遞延任務(wù)未及時(shí)處理,進(jìn)入下一輪后處理 delayed_task_time_ = TimeTicks(); } } if (should_quit_) break; } }
由于遞延任務(wù)一般優(yōu)先級(jí)高于空閑任務(wù),所以我們先于空閑任務(wù)處理它們。另外deferred_task_time_記錄了下一個(gè)遞延任務(wù)的單調(diào)遞增時(shí)間(比如當(dāng)前線程的clock值),當(dāng)沒(méi)有I/O事件需要處理且也沒(méi)有Idle任務(wù)需要處理時(shí),如果有尚未到期的遞延任務(wù),那么需要在源上開啟一個(gè)定時(shí)器在遞延任務(wù)到期后解除消息泵的阻塞。因此,要支持遞延任務(wù)的源必須具備第三個(gè)特點(diǎn),那就是支持定時(shí)喚醒。
參考資料:
http://docs.libuv.org/en/late...
https://msdn.microsoft.com/en...
https://developer.apple.com/l...
https://docs.google.com/docum...
網(wǎng)易云免費(fèi)體驗(yàn)館,0成本體驗(yàn)20+款云產(chǎn)品!
更多網(wǎng)易技術(shù)、產(chǎn)品、運(yùn)營(yíng)經(jīng)驗(yàn)分享請(qǐng)點(diǎn)擊。
文章來(lái)源: 網(wǎng)易云社區(qū)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/25255.html
摘要:通過(guò)向消息池發(fā)送各種消息事件通過(guò)處理相應(yīng)的消息事件。消息泵通過(guò)不斷地從中抽取,按分發(fā)機(jī)制將消息分發(fā)給目標(biāo)處理者。也稱之為消息隊(duì)列,特點(diǎn)是先進(jìn)先出,底層實(shí)現(xiàn)是單鏈表數(shù)據(jù)結(jié)構(gòu)。目錄介紹 6.0.0.1 談?wù)勏C(jī)制Hander作用?有哪些要素?流程是怎樣的?簡(jiǎn)單說(shuō)一下你的看法! 6.0.0.2 為什么一個(gè)線程只有一個(gè)Looper、只有一個(gè)MessageQueue,可以有多個(gè)Handler? 6...
摘要:通過(guò)向消息池發(fā)送各種消息事件通過(guò)處理相應(yīng)的消息事件。子線程往消息隊(duì)列發(fā)送消息,并且往管道文件寫數(shù)據(jù),主線程即被喚醒,從管道文件讀取數(shù)據(jù),主線程被喚醒只是為了讀取消息,當(dāng)消息讀取完畢,再次睡眠。 目錄介紹 6.0.0.1 談?wù)勏C(jī)制Hander作用?有哪些要素?流程是怎樣的? 6.0.0.2 為什么一個(gè)線程只有一個(gè)Looper、只有一個(gè)MessageQueue,可以有多個(gè)Handle...
摘要:機(jī)制處理的個(gè)關(guān)鍵對(duì)象線程之間傳遞的消息,可以攜帶一些簡(jiǎn)單的數(shù)據(jù)供子線程與主線程進(jìn)行交換數(shù)據(jù)。解決方法子線程通過(guò)發(fā)送消息給主線程,讓主線程處理消息,進(jìn)而更新。 showImg(https://segmentfault.com/img/remote/1460000019975019?w=157&h=54); 極力推薦文章:歡迎收藏Android 干貨分享 showImg(https://...
閱讀 924·2021-09-22 15:18
閱讀 1304·2021-09-09 09:33
閱讀 2850·2019-08-30 10:56
閱讀 1349·2019-08-29 16:30
閱讀 1581·2019-08-29 13:02
閱讀 1541·2019-08-26 13:55
閱讀 1773·2019-08-26 13:41
閱讀 2067·2019-08-26 11:56