Chrome源碼剖析 【序】 && 【一】

【序】 

開源是口好東西,它讓這個充斥着大量工業垃圾代碼和教材玩具代碼的行業,多了一些藝術氣息和美的潛質。它使得每個人,無論你來自米國紐約還是中國鐵嶺,都有機會站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。
現在我就是來抱大腿的,這條粗腿隸屬於Chrome(開源項目名稱其實是Chromium,本來Chrome這個名字就夠晦澀了,沒想到它的本名還更上一層樓...),Google那充滿狼子野心的瀏覽器。每一個含着金勺子出生的人都免不了被仰慕並被唾罵,Chrome也不例外。關於Chrome的優劣好壞討論的太多了,基本已經被嚼成甘蔗渣了,沒有人願意再多張一口了。俗話說,內行看門道外行看熱鬧,大部分所謂的外行,是通過使用的真實感受來評定優劣的,這無疑是最好的方式。但偏偏還是有自詡的內行,喜歡說內行話辦外行事,一看到Chrome用到多進程就說垃圾廢物肯定低能。拜託,大家都是搞技術的,你知道多進程的缺點,Google也知道,他們不是政客,除了搞個噱頭扯個蛋就一無所知了,人家也是有臉有皮的,寫一坨屎一樣的開源代碼放出來遭世人恥笑難道會很開心?所謂技術的優劣,是不能一概而論的,同樣的技術在不同場合不同環境不同代碼實現下,效果是有所不同的。既然Chrome用了很多看上去不是很美的技術,我們是不是也需要了解一下它爲什麼要用,怎麼用的,然後再開口說話?(恕不邀請,請自行對號入座...)。。。
人說是騾子是馬拉出來遛遛,Google已經把Chrome這匹驢子拉到了世人面前,大家可以隨意的遛。我們一直自詡是搞科學的,就是在努力和所謂的藝術家拉開,人搞超女評委的,可以隨意塞着屁眼用嘴放屁,楞把李天王說是李天后,你也只能說他是藝術品位獨特。你要搞科學就不行,說的不對,輕的叫無知,重的叫學術欺詐,結果一片慘淡。所以,既然代碼都有了,再說話,就只能當點心注點意了,先看,再說。。。
我已經開始遛Chrome這頭驢了,確切一點,是頭壯碩的肥驢,項目總大小接近2G。這樣的龐然大物要從頭到腳每個毛孔的大量一遍,那估計不嚥氣也要吐血的,咱又不是做Code review,不需要如此拼命。每一個好的開源項目,都像是一個美女,這世界沒有十全十美的美女,自然也不會有樣樣傑出的開源項目。每個美女都有那麼一兩點讓你最心動不已或者倍感神祕的,你會把大部分的注意力都放在上面細細品味,看開源,也是一樣。Chrome對我來說,有吸引力的地方在於(排名分先後...):
1. 它是如何利用多進程(其實也會有多線程一起)做併發的,又是如何解決多進程間的一些問題的,比如進程間通信,進程的開銷;
2. 做爲一個後來者,它的擴展能力如何,如何去權衡對原有插件的兼容,提供怎麼樣的一個插件模型;
3. 它的整體框架是怎樣,有沒有很NB的架構思想;
4. 它如何實現跨平臺的UI控件系統;
5. 傳說中的V8,爲啥那麼快。
但Chrome是一個跨平臺的瀏覽器,其Linux和Mac版本正在開發過程中,所以我把所有的眼光都放在了windows版本中,所有的代碼剖析都是基於windows版本的。話說,我本是瀏覽器新手、win api白癡以及併發處理的火星人,爲了我的好奇投身到這個溜驢的行業中來,難免有學的不到位看的走眼的時候,各位看官手下超生,有錯誤請指正,實在看不下去,回家自己牽着遛吧。。。
扯淡實在是個體力活,所以後面我會少扯淡多說問題。。。
關於Chrome的×××和環境配置,大家看這裏(windows版本),只想強調一點,一定要嚴格按照說明來配置環境,特別是vs2005的補丁和windows SDK的安裝,否則肯定是編譯不過的。。。
最後,寫這部分唯一不是廢話的內容,請記住以下這幅圖,這是Chrome最精華的一個縮影,如果你還有空,一定要去這裏進行閱讀,其中重中之重是這一篇。。。
圖1 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.com和www.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
是否阻塞在信號量上

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在底層庫上做了足夠的功夫:
  1. 它提供了一大套的模板封裝(參見task.h),可以將Task擺脫繼承結構、函數名、函數參數等限制(就是基於模板的僞function實現,想要更深入瞭解,建議直接看鼻祖《Modern C++》和它的Loki庫...);
  2. 同時派生出CancelableTask、ReleaseTask、DeleteTask等子類,提供更爲良好的默認實現;
  3. 在消息循環中,按邏輯的不同,將Task又分成即時處理的Task、延時處理的Task、Idle時處理的Task,滿足不同場景的需求;
  4. Task派生自tracked_objects::Tracked,Tracked是爲了實現多線程環境下的日誌記錄、統計等功能,使得Task天生就有良好的可調試性和可統計性;
這一套七葷八素的都搭建完,這纔算是一個完整的Task模型,由此可知,這餃子,做的還是很費功夫的。。。

3. Chrome的多線程模型

工欲善其事,必先利其器。Chrome之所以費了老鼻子勁去磨底層框架這把刀,就是爲了面對多線程這坨怪獸的時候殺的更順暢一些。在Chrome的多線程模型下,加鎖這個事情只發生在將Task放入某線程的任務隊列中,其他對任何數據的操作都不需要加鎖。當然,天下沒有免費的午餐,爲了合理傳遞Task,你需要了解每一個數據對象所管轄的線程,不過這個事情,與紛繁的加鎖相比,真是小兒科了不知道多少倍。。。
 
圖3 Task的執行模型

如果你熟悉設計模式,你會發現這是一個Command模式,將創建於執行的環境相分離,在一個線程中創建行爲,在另一個線程中執行行爲。Command模式的優點在於,將實現操作與構造操作解耦,這就避免了鎖的問題,使得多線程與單線程編程模型統一起來,其次,Command還有一個優點,就是有利於命令的組合和擴展,在Chrome中,它有效統一了同步和異步處理的邏輯。。。

Command模式
Command模式,是一種看上去很酷的模式,傳統的面向對象編程,我們封裝的往往都是數據,在Command模式下,我們希望封裝的是行爲。這件事在函數式編程中很正常,封裝一個函數作爲參數,傳來傳去,稀疏平常的事兒;但在面向對象的編程中,我們需要通過繼承、模板、函數指針等手法,才能將其實現。。。
應用Command模式,我們是期望這個行爲能到一個不同於它出生的環境中去執行,簡而言之,這是一種想生不想養的行爲。我們做Undo/Redo的時候,會把在任一一個環境中創建的Command,放到一個隊列環境中去,供統一的調度;在Chrome中,也是如此,我們在一個線程環境中創建了Task,卻把它放到別的線程中去執行,這種寄居蟹似的生活方式,在很多場合都是有用武之地的。。。

在一般的多線程模型中,我們需要分清楚啥是同步啥是異步,在同步模式下,一切看上去和單線程沒啥區別,但同時也喪失了多線程的優勢(淪落成爲多線程串行...)。而如果採用異步的模式,那寫起來就麻煩多了,你需要註冊回調,小心管理對象的生命週期,程序寫出來是嗷嗷噁心。在Chrome的多線程模型下,同步和異步的編程模型區別就不復存在了,如果是這樣一個場景:A線程需要B線程做一些事情,然後回到A線程繼續做一些事情;在Chrome下你可以這樣來做:生成一個Task,放到B線程的隊列中,在該Task的Run方法最後,會生成另一個Task,這個Task會放回到A的線程隊列,由A來執行。如此一來,同步異步,天下一統,都是Task傳來傳去,想不會,都難了。。。

圖4 Chrome的一種異步執行的解決方案

4. Chrome多線程模型的優缺點

一直在說Chrome在規避鎖的問題,那到底鎖是哪裏不好,犯了何等滔天罪責,落得如此人見人嫌恨不得先殺而後快的境地。《代碼之美》的第二十四章“美麗的併發”中,Haskell設計人之一的Simon Peyton Jones總結了一下用鎖的困難之處,我罰抄一遍,如下:
  1. 鎖少加了,導致兩個線程同時修改一個變量;
  2. 鎖多加了,輕則妨礙併發,重則導致死鎖;
  3. 鎖加錯了,由於鎖和需要鎖的數據之間的聯繫,只存在於程序員的大腦中,這種事情太容易發生了;
  4. 加鎖的順序錯了,維護鎖的順序是一件困難而又容易出錯的問題;
  5. 錯誤恢復;
  6. 忘記喚醒和錯誤的重試;
  7. 而最根本的缺陷,是鎖和條件變量不支持模塊化的編程。比如一個轉賬業務中,A賬戶扣了100元錢,B賬戶增加了100元,即使這兩個動作單獨用鎖保護維持其正確性,你也不能將兩個操作簡單的串在一起完成一個轉賬操作,你必須讓它們的鎖都暴露出來,重新設計一番。好好的兩個函數,愣是不能組在一起用,這就是鎖的最大悲哀;
通過這些缺點的描述,也就可以明白Chrome多線程模型的優點。它解決了鎖的最根本缺陷,即,支持模塊化的編程,你只需要維護對象和線程之間的職能關係即可,這個攤子,比之鎖的那個爛攤子,要簡化了太多。對於程序員來說,負擔一瞬間從泰山降成了鴻毛。。。
而Chrome多線程模型的一個主要難點,在於線程與數據關係的設計上,你需要良好的劃分各個線程的職責,如果有一個線程所管轄的數據,幾乎佔據了大半部分的Task,那麼它就會從多線程淪爲單線程,Task隊列的鎖也將成爲一個大大的瓶頸。。。

設計者的職責
一個底層結構設計是否成功,這個設計者是否稱職,我一直覺得是有一個很簡單的衡量標準的。你不需要看這個設計人用了多少NB的技術,你只需要關心,他的設計,是否給其他開發人員帶來了困難。一個NB的設計,是將所有困難都集中在底層搞定,把其他開發人員換成白癡都可以工作的那種;一個SB的設計,是自己弄了半天,只是爲了給其他開發人員一個長達250條的注意事項,然後很NB的說,你們按照這個手冊去開發,就不會有問題了。。。

從根本上來說,Chrome的線程模型解決的是併發中的用戶體驗問題而不是聯合工作的問題(參見我前面噴的“閒話併發”),它不是和Map/Reduce那樣將關注點放在數據和執行步驟的拆分上,而是放在線程和數據的對應關係上,這是和瀏覽器的工作環境相匹配的。設計總是和所處的環境相互依賴的,畢竟,在客戶端,不會和服務器一樣,存在超規模的併發處理任務,而只是需要儘可能的改善用戶體驗,從這個角度來說,Chrome的多線程模型,至少看上去很美。。。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章