1. 程式人生 > >Chrome原始碼剖析--Chrome的多執行緒模型 上

Chrome原始碼剖析--Chrome的多執行緒模型 上

0. Chrome的併發模型

如果你仔細看了前面的圖,對Chrome的執行緒和程序框架應該有了個基本的瞭解。Chrome有一個主程序,稱為Browser程序,它是老大,管理Chrome大部分的日常事務;其次,會有很多Renderer程序,它們圈地而治,各管理一組站點的顯示和通訊(Chrome在宣傳中一直宣稱一個tab對應一個程序,其實是很不確切的...),它們彼此互不搭理,只和老大說話,由老大負責權衡各方利益。它們和老大說話的渠道,稱做IPC(Inter-Process Communication),這是Google搭的一套程序間通訊的機制,基本的實現後面自會分解。。。

Chrome的程序模型

Google在宣傳的時候一直都說,Chrome是one tab one process的模式,其實,這只是為了宣傳起來方便如是說而已,基本等同廣告,實際療效,還要從程式碼中來看。實際上,Chrome支援的程序模型遠比宣傳豐富,你可以參考一下這裡 ,簡單的說,Chrome支援以下幾種程序模型:

  1. Process-per-site-instance:就是你開啟一個網站,然後從這個網站鏈開的一系列網站都屬於一個程序。這是Chrome的預設模式。
  2. Process-per-site:同域名範疇的網站放在一個程序,比如www.google.comwww.google.com/bookmarks就屬於一個域名內(google有自己的判定機制),不論有沒有互相開啟的關係,都算作是一個程序中。用命令列--process-per-site開啟。
  3. Process-per-tab:這個簡單,一個tab一個process,不論各個tab的站點有無聯絡,就和宣傳的那樣。用--process-per-tab開啟。
  4. Single Process:這個很熟悉了吧,傳統瀏覽器的模式,沒有多程序只有多執行緒,用--single-process開啟。

關於各種模式的優缺點,官方有官方的說法,大家自己也會有自己的評述。不論如何,至少可以說明,Google不是由於白痴而採取多程序的策略,而是實驗出來的效果。。。

大家可以用Shift+Esc觀察各模式下程序狀況,至少我是觀察失敗了(每種都和預設的一樣...),原因待跟蹤。。。

不論是Browser程序還是Renderer程序,都不只是光桿司令,它們都有一系列的執行緒為自己打理各種業務。對於Renderer程序,它們通常有兩個執行緒,一個是Main thread,它負責與老大進行聯絡,有一些幕後黑手的意思;另一個是Render thread,它們負責頁面的渲染和互動,一看就知道是這個幫派的門臉級人物。相比之下,Browser程序既然是老大,小弟自然要多一些,除了大腦般的Main thread,和負責與各Renderer幫派通訊的IO thread,其實還包括負責管檔案的file thread,負責管資料庫的db thread等等(一個更詳細的列表,參見

這裡 ),它們各盡其責,齊心協力為老大打拼。它們和各Renderer程序的之間的關係不一樣,同一個程序內的執行緒,往往需要很多的協同工作,這一坨執行緒間的併發管理,是Chrome最出彩的地方之一了。。。

閒話併發

單程序單執行緒的程式設計是最愜意的事情,所看即所得,一維的思考即可。但程式設計師的世界總是沒有那麼美好,在很多的場合,我們都需要有多執行緒、多程序、多機器攜起手來一齊上陣共同完成某項任務,統稱:併發(非官方版定義...)。在我看來,需要併發的場合主要是要兩類:

  1. 為了更好的使用者體驗。有的事情處理起來太慢,比如資料庫讀寫、遠端通訊、複雜計算等等,如果在一個執行緒一個程序裡面來做,往往會影響使用者感受,因此需要另開一個執行緒或程序轉到後臺進行處理。它之所以能夠生效,仰仗的是單CPU的分時機制,或者是多CPU協同工作。在單CPU的條件下,兩個任務分成兩撥完成的總時間,是大於兩個任務輪流完成的,但是由於彼此交錯,更人的感覺更為的自然一些。
  2. 為了加速完成某項工作。大名鼎鼎的Map/Reduce,做的就是這樣的事情,它將一個大的任務,拆分成若干個小的任務,分配個若干個程序去完成,各自收工後,在彙集在一起,更快的得到最後的結果。為了達到這個目的,只有在多CPU的情形下才有可能,在單CPU的場合(單機單CPU...),是無法實現的。

在第二種場合下,我們會自然而然的關注資料的分離,從而很好的利用上多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的訊息迴圈

當然,不是每一個訊息迴圈類都需要跑那麼一大圈的,有些執行緒,它不會涉及到那麼多的事情和邏輯,白白浪費體力和時間,實在是不可饒恕的。因此,在實現中,不同的MessagePump類,實現是有所不同的,詳見下表:

MessagePumpDefault MessagePumpForIO MessagePumpForUI
是否需要處理系統訊息
是否需要處理Task
是否需要處理Watcher
是否阻塞在訊號量上