Chrome原始碼剖析 【一】 Chrome的多執行緒模型
阿新 • • 發佈:2019-02-13
【一】 Chrome的多執行緒模型
0. Chrome的併發模型
如果你仔細看了前面的圖,對Chrome的執行緒和程序框架應該有了個基本的瞭解。Chrome有一個主程序,稱為Browser程序,它是老大,管理Chrome大部分的日常事務;其次,會有很多Renderer程序,它們圈地而治,各管理一組站點的顯示和通訊(Chrome在宣傳中一直宣稱一個tab對應一個程序,其實是很不確切的...),它們彼此互不搭理,只和老大說話,由老大負責權衡各方利益。它們和老大說話的渠道,稱做IPC(Inter-Process Communication),這是Google搭的一套程序間通訊的機制,基本的實現後面自會分解。。。
Chrome的程序模型
|
閒話併發
在第二種場合下,我們會自然而然的關注資料的分離,從而很好的利用上多CPU的能力;而在第一種場合,我們習慣了單CPU的模式,往往不注重資料與行為的對應關係,導致在多CPU的場景下,效能不升反降。。。 |
1. Chrome的執行緒模型
仔細回憶一下我們大部分時候是怎麼來用執行緒的,在我足夠貧瘠的多執行緒經歷中,往往都是這樣用的:起一個執行緒,傳入一個特定的入口函式,看一下這個函式是否是有副作用的(Side Effect),如果有,並且還會涉及到多執行緒的資料訪問,仔細排查,在可疑地點上鎖伺候。。。
Chrome的執行緒模型走的是另一個路子,即,極力規避鎖的存在。換更精確的描述方式來說,Chrome的執行緒模型,將鎖限制了極小的範圍內(僅僅在將Task放入訊息佇列的時候才存在...),並且使得上層完全不需要關心鎖的問題(當然,前提是遵循它的程式設計模型,將函式用Task封裝併發送到合適的執行緒去執行...),大大簡化了開發的邏輯。。。
不過,從實現來說,Chrome的執行緒模型並沒有什麼神祕的地方(美女嘛,都是穿衣服比不穿衣服更有盼頭...),它用到了訊息迴圈的手段。每一個Chrome的執行緒,入口函式都差不多,都是啟動一個訊息迴圈(參見MessagePump類),等待並執行任務。而其中,唯一的差別在於,根據執行緒處理事務類別的不同,所起的訊息迴圈有所不同。比如處理程序間通訊的執行緒(注意,在Chrome中,這類執行緒都叫做IO執行緒,估計是當初設計的時候誰的腦門子拍錯了...)啟用的是MessagePumpForIO類,處理UI的執行緒用的是MessagePumpForUI類,一般的執行緒用到的是MessagePumpDefault類(只討論windows,
windows, windows...)。不同的訊息迴圈類,主要差異有兩個,一是訊息迴圈中需要處理什麼樣的訊息和任務,第二個是迴圈流程(比如是死迴圈還是阻塞在某訊號量上...)。下圖是一個完整版的Chrome訊息迴圈圖,包含處理Windows的訊息,處理各種Task(Task是什麼,稍後揭曉,敬請期待...),處理各個訊號量觀察者(Watcher),然後阻塞在某個訊號量上等待喚醒。。。
圖2 Chrome的訊息迴圈 |
MessagePumpDefault | MessagePumpForIO | MessagePumpForUI | |
是否需要處理系統訊息 | 否 | 是 | 是 |
是否需要處理Task | 是 | 是 | 是 |
是否需要處理Watcher | 否 | 是 | 否 |
是否阻塞在訊號量上 | 否 | 是 | 是 |
2. Chrome中的Task
從上面的表不難看出,不論是哪一種訊息迴圈,必須處理的,就是Task(暫且遺忘掉系統訊息的處理和Watcher,以後,我們會緬懷它們的...)。刨去其它東西的干擾,只留下Task的話,我們可以這樣認為:Chrome中的執行緒從實現層面來看沒有任何區別,它的區別只存在於職責層面,不同職責的執行緒,會處理不同的Task。最後,在鋪天蓋地西紅柿來臨之前,我說一下啥是Task。。。
簡單的看,Task就是一個類,一個包含了void Run()抽象方法的類(參見Task類...)。一個真實的任務,可以派生Task類,並實現其Run方法。每個MessagePump類中,會有一個MessagePump::Delegate的類的物件(MessagePump::Delegate的一個實現,請參見MessageLoop類...),在這個物件中,會維護若干個Task的佇列。當你期望,你的一個邏輯在某個執行緒內執行的時候,你可以派生一個Task,把你的邏輯封裝在Run方法中,然後例項一個物件,呼叫期望執行緒中的PostTask方法,將該Task物件放入到其Task佇列中去,等待執行。我知道很多人已經抄起了板磚,因為這種手法實在是太常見了,就不是一個簡單的依賴倒置,線上程池,Undo\Redo等模組的實現中,用的太多了。。。
但,我想說的是,雖說誰家過年都是吃頓餃子,這餃子好不好吃還是得看手藝,不能一概而論。在Chrome中,執行緒模型是統一且唯一的,這就相當於有了一套標準,它需要滿足在各個執行緒上執行的幾十上百種任務的需求,因此,必須在靈活行和易用性上有良好的表現,這就是設計標準的難度。為了滿足這些需求,Chrome在底層庫上做了足夠的功夫:
- 它提供了一大套的模板封裝(參見task.h),可以將Task擺脫繼承結構、函式名、函式引數等限制(就是基於模板的偽function實現,想要更深入瞭解,建議直接看鼻祖《Modern C++》和它的Loki庫...);
- 同時派生出CancelableTask、ReleaseTask、DeleteTask等子類,提供更為良好的預設實現;
- 在訊息迴圈中,按邏輯的不同,將Task又分成即時處理的Task、延時處理的Task、Idle時處理的Task,滿足不同場景的需求;
- Task派生自tracked_objects::Tracked,Tracked是為了實現多執行緒環境下的日誌記錄、統計等功能,使得Task天生就有良好的可除錯性和可統計性;
3. Chrome的多執行緒模型
工欲善其事,必先利其器。Chrome之所以費了老鼻子勁去磨底層框架這把刀,就是為了面對多執行緒這坨怪獸的時候殺的更順暢一些。在Chrome的多執行緒模型下,加鎖這個事情只發生在將Task放入某執行緒的任務佇列中,其他對任何資料的操作都不需要加鎖。當然,天下沒有免費的午餐,為了合理傳遞Task,你需要了解每一個數據物件所管轄的執行緒,不過這個事情,與紛繁的加鎖相比,真是小兒科了不知道多少倍。。。
圖3 Task的執行模型 |
Command模式 |
圖4 Chrome的一種非同步執行的解決方案 |
4. Chrome多執行緒模型的優缺點
一直在說Chrome在規避鎖的問題,那到底鎖是哪裡不好,犯了何等滔天罪責,落得如此人見人嫌恨不得先殺而後快的境地。《程式碼之美》的第二十四章“美麗的併發”中,Haskell設計人之一的Simon Peyton Jones總結了一下用鎖的困難之處,我罰抄一遍,如下:- 鎖少加了,導致兩個執行緒同時修改一個變數;
- 鎖多加了,輕則妨礙併發,重則導致死鎖;
- 鎖加錯了,由於鎖和需要鎖的資料之間的聯絡,只存在於程式設計師的大腦中,這種事情太容易發生了;
- 加鎖的順序錯了,維護鎖的順序是一件困難而又容易出錯的問題;
- 錯誤恢復;
- 忘記喚醒和錯誤的重試;
- 而最根本的缺陷,是鎖和條件變數不支援模組化的程式設計。比如一個轉賬業務中,A賬戶扣了100元錢,B賬戶增加了100元,即使這兩個動作單獨用鎖保護維持其正確性,你也不能將兩個操作簡單的串在一起完成一個轉賬操作,你必須讓它們的鎖都暴露出來,重新設計一番。好好的兩個函式,愣是不能組在一起用,這就是鎖的最大悲哀;
設計者的職責 |